diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index 499b63bdd7..152ed701c1 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -97,6 +97,9 @@ Manage shared web env var additions and rotations with `pnpm web:env set ({ jest.mock('@/lib/integrations/gitlab-service', () => ({ getValidGitLabToken: jest.fn<() => Promise>().mockResolvedValue('mock-token'), - getStoredProjectAccessToken: jest.fn<() => null>().mockReturnValue(null), + getValidGitLabProjectAccessToken: jest + .fn<() => Promise>() + .mockResolvedValue('mock-token'), })); jest.mock('@sentry/nextjs', () => ({ diff --git a/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts b/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts index ce65be90e4..7b20af76dd 100644 --- a/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts +++ b/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts @@ -53,8 +53,8 @@ import { import type { GitLabCommitStatusState } from '@/lib/integrations/platforms/gitlab/adapter'; import { getIntegrationById } from '@/lib/integrations/db/platform-integrations'; import { + getValidGitLabProjectAccessToken, getValidGitLabToken, - getStoredProjectAccessToken, } from '@/lib/integrations/gitlab-service'; import { captureException, captureMessage } from '@sentry/nextjs'; import { CALLBACK_TOKEN_SECRET } from '@/lib/config.server'; @@ -865,14 +865,28 @@ async function upsertModelNotFoundSummary( /** * Resolves a GitLab access token for a review's project. - * Prefers a stored Project Access Token; falls back to the user's OAuth token. + * Uses the exact project credential when a project ID is present. */ async function resolveGitLabAccessToken( integration: PlatformIntegration, projectId: number | null ): Promise { - const storedPrat = projectId ? getStoredProjectAccessToken(integration, projectId) : null; - return storedPrat ? storedPrat.token : await getValidGitLabToken(integration); + let userId: string; + let organizationId: string | undefined; + if (integration.owned_by_organization_id) { + organizationId = integration.owned_by_organization_id; + const botUserId = await getBotUserId(organizationId, 'code-review'); + if (!botUserId) throw new Error('GitLab organization has no configured acting user'); + userId = botUserId; + } else if (integration.owned_by_user_id) { + userId = integration.owned_by_user_id; + } else { + throw new Error('GitLab integration has no owner'); + } + const actor = { userId, ...(organizationId ? { organizationId } : {}) }; + return projectId + ? await getValidGitLabProjectAccessToken(integration, projectId, actor) + : await getValidGitLabToken(integration, actor); } /** diff --git a/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts b/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts index b7caa7814e..9b80a56714 100644 --- a/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts +++ b/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts @@ -25,6 +25,7 @@ import { createHmac } from 'crypto'; import { captureException } from '@sentry/nextjs'; import type { PlatformIntegration } from '@kilocode/db'; import z from 'zod'; +import { getBotUserId } from '@/lib/bot-users/bot-user-service'; /** * Derive a per-request callback token so the dedicated callback HMAC secret @@ -136,10 +137,15 @@ export default async function spawnCloudAgentSession( const kilocodeOrganizationId = owner.type === 'org' ? owner.id : undefined; if (args.gitlabProject) { + const gitLabActorUserId = + owner.type === 'org' ? await getBotUserId(owner.id, 'code-review') : owner.id; + if (!gitLabActorUserId) { + return { response: 'Error: No acting user is configured for this GitLab organization.' }; + } // GitLab path: get token + instance URL, build clone URL, use gitUrl/gitToken const gitlabToken = owner.type === 'org' - ? await getGitLabTokenForOrganization(owner.id) + ? await getGitLabTokenForOrganization(owner.id, gitLabActorUserId) : await getGitLabTokenForUser(owner.id); if (!gitlabToken) { diff --git a/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.test.ts b/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.test.ts index d74110d24e..d4a6661f6e 100644 --- a/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.test.ts +++ b/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.test.ts @@ -5,7 +5,13 @@ import { buildGitLabCloneUrl } from './gitlab-integration-helpers'; // Define mock functions at module level with proper typing const mockGetGitLabIntegration = jest.fn<(owner: Owner) => Promise>(); -const mockGetValidGitLabToken = jest.fn<(integration: PlatformIntegration) => Promise>(); +const mockGetValidGitLabToken = + jest.fn< + ( + integration: PlatformIntegration, + actor: { userId: string; organizationId?: string } + ) => Promise + >(); const mockGetIntegrationForOrganization = jest.fn<(organizationId: string, platform: string) => Promise>(); const mockGetIntegrationForOwner = @@ -207,7 +213,9 @@ describe('gitlab-integration-helpers', () => { const result = await getGitLabTokenForUser('user-123'); expect(result).toBe('valid-token-123'); - expect(mockGetValidGitLabToken).toHaveBeenCalledWith(mockIntegration); + expect(mockGetValidGitLabToken).toHaveBeenCalledWith(mockIntegration, { + userId: 'user-123', + }); }); it('should throw TRPCError when token retrieval fails', async () => { @@ -231,7 +239,7 @@ describe('gitlab-integration-helpers', () => { mockGetIntegrationForOrganization.mockResolvedValue(null); const { getGitLabTokenForOrganization } = await import('./gitlab-integration-helpers'); - const result = await getGitLabTokenForOrganization('org-123'); + const result = await getGitLabTokenForOrganization('org-123', 'actor-123'); expect(result).toBeUndefined(); expect(mockGetIntegrationForOrganization).toHaveBeenCalledWith('org-123', 'gitlab'); @@ -246,10 +254,13 @@ describe('gitlab-integration-helpers', () => { mockGetValidGitLabToken.mockResolvedValue('org-valid-token-456'); const { getGitLabTokenForOrganization } = await import('./gitlab-integration-helpers'); - const result = await getGitLabTokenForOrganization('org-123'); + const result = await getGitLabTokenForOrganization('org-123', 'actor-123'); expect(result).toBe('org-valid-token-456'); - expect(mockGetValidGitLabToken).toHaveBeenCalledWith(mockIntegration); + expect(mockGetValidGitLabToken).toHaveBeenCalledWith(mockIntegration, { + userId: 'actor-123', + organizationId: 'org-123', + }); }); it('should throw TRPCError when token retrieval fails', async () => { @@ -262,7 +273,7 @@ describe('gitlab-integration-helpers', () => { const { getGitLabTokenForOrganization } = await import('./gitlab-integration-helpers'); - await expect(getGitLabTokenForOrganization('org-123')).rejects.toThrow( + await expect(getGitLabTokenForOrganization('org-123', 'actor-123')).rejects.toThrow( 'Failed to authenticate with GitLab integration' ); }); @@ -353,7 +364,11 @@ describe('gitlab-integration-helpers', () => { const { validateGitLabRepoAccessForOrganization } = await import('./gitlab-integration-helpers'); - const result = await validateGitLabRepoAccessForOrganization('org-123', 'org/project'); + const result = await validateGitLabRepoAccessForOrganization( + 'org-123', + 'actor-123', + 'org/project' + ); expect(result).toBe(true); }); @@ -368,7 +383,11 @@ describe('gitlab-integration-helpers', () => { const { validateGitLabRepoAccessForOrganization } = await import('./gitlab-integration-helpers'); - const result = await validateGitLabRepoAccessForOrganization('org-123', 'org/nonexistent'); + const result = await validateGitLabRepoAccessForOrganization( + 'org-123', + 'actor-123', + 'org/nonexistent' + ); expect(result).toBe(false); }); @@ -383,7 +402,11 @@ describe('gitlab-integration-helpers', () => { const { validateGitLabRepoAccessForOrganization } = await import('./gitlab-integration-helpers'); - const result = await validateGitLabRepoAccessForOrganization('org-123', 'org/project'); + const result = await validateGitLabRepoAccessForOrganization( + 'org-123', + 'actor-123', + 'org/project' + ); expect(result).toBe(true); }); @@ -393,7 +416,11 @@ describe('gitlab-integration-helpers', () => { const { validateGitLabRepoAccessForOrganization } = await import('./gitlab-integration-helpers'); - const result = await validateGitLabRepoAccessForOrganization('org-123', 'org/project'); + const result = await validateGitLabRepoAccessForOrganization( + 'org-123', + 'actor-123', + 'org/project' + ); expect(result).toBe(false); }); @@ -408,7 +435,11 @@ describe('gitlab-integration-helpers', () => { const { validateGitLabRepoAccessForOrganization } = await import('./gitlab-integration-helpers'); - const result = await validateGitLabRepoAccessForOrganization('org-123', 'org/project'); + const result = await validateGitLabRepoAccessForOrganization( + 'org-123', + 'actor-123', + 'org/project' + ); expect(result).toBe(false); }); @@ -427,6 +458,7 @@ describe('gitlab-integration-helpers', () => { await import('./gitlab-integration-helpers'); const result = await validateGitLabRepoAccessForOrganization( 'org-123', + 'actor-123', 'org/subgroup/project' ); 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 3c9810ff75..dd446b04bd 100644 --- a/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.ts +++ b/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.ts @@ -59,7 +59,8 @@ type GitLabMetadata = { * Automatically refreshes the token if expired */ export async function getGitLabTokenForOrganization( - organizationId: string + organizationId: string, + actorUserId: string ): Promise { const integration = await getIntegrationForOrganization(organizationId, PLATFORM.GITLAB); @@ -68,7 +69,10 @@ export async function getGitLabTokenForOrganization( } try { - const token = await getValidGitLabToken(integration); + const token = await getValidGitLabToken(integration, { + userId: actorUserId, + organizationId, + }); return token; } catch (_error) { throw new TRPCError({ @@ -90,7 +94,7 @@ export async function getGitLabTokenForUser(userId: string): Promise { const integration = await getIntegrationForOrganization(organizationId, PLATFORM.GITLAB); @@ -121,7 +126,10 @@ export async function fetchGitLabRepositoriesForOrganization( const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh or no cached repos, fetch from GitLab and update cache if (forceRefresh || !cachedRepositories?.length) { - const accessToken = await getValidGitLabToken(integration); + const accessToken = await getValidGitLabToken(integration, { + userId: actorUserId, + organizationId, + }); const repositories = await fetchGitLabProjects(accessToken, instanceUrl); await updateRepositoriesForIntegration(integration.id, repositories); return { @@ -168,7 +176,7 @@ export async function fetchGitLabRepositoriesForUser( const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh or no cached repos, fetch from GitLab and update cache if (forceRefresh || !cachedRepositories?.length) { - const accessToken = await getValidGitLabToken(integration); + const accessToken = await getValidGitLabToken(integration, { userId }); const repositories = await fetchGitLabProjects(accessToken, instanceUrl); await updateRepositoriesForIntegration(integration.id, repositories); return { @@ -228,10 +236,11 @@ export async function validateGitLabRepoAccessForUser( */ export async function validateGitLabRepoAccessForOrganization( organizationId: string, + actorUserId: string, projectPath: string ): Promise { try { - const result = await fetchGitLabRepositoriesForOrganization(organizationId, false); + const result = await fetchGitLabRepositoriesForOrganization(organizationId, actorUserId, false); if (!result.integrationInstalled || !result.repositories.length) { return false; diff --git a/apps/web/src/lib/code-reviews/manual-code-review-jobs.ts b/apps/web/src/lib/code-reviews/manual-code-review-jobs.ts index 0417acc238..953f1e1b12 100644 --- a/apps/web/src/lib/code-reviews/manual-code-review-jobs.ts +++ b/apps/web/src/lib/code-reviews/manual-code-review-jobs.ts @@ -386,7 +386,10 @@ async function resolveConnectedGitLabSource( const instanceUrl = getGitLabIntegrationInstanceUrl(integration); if (parsed.origin !== instanceUrl) continue; - const accessToken = await getValidGitLabToken(integration); + const accessToken = await getValidGitLabToken(integration, { + userId: owner.userId, + ...(owner.type === 'org' ? { organizationId: owner.id } : {}), + }); const rawMergeRequest = await fetchGitLabMergeRequest({ accessToken, projectId: parsed.projectPath, diff --git a/apps/web/src/lib/code-reviews/triggers/prepare-review-payload.ts b/apps/web/src/lib/code-reviews/triggers/prepare-review-payload.ts index c9affdc4b8..71c30020f6 100644 --- a/apps/web/src/lib/code-reviews/triggers/prepare-review-payload.ts +++ b/apps/web/src/lib/code-reviews/triggers/prepare-review-payload.ts @@ -500,7 +500,10 @@ export async function prepareReviewPayload( } try { - gitlabToken = await getOrCreateProjectAccessToken(integration, projectId); + gitlabToken = await getOrCreateProjectAccessToken(integration, projectId, { + userId: owner.userId, + ...(owner.type === 'org' ? { organizationId: owner.id } : {}), + }); logExceptInTest('[prepareReviewPayload] Using PrAT for code review', { reviewId, repoFullName: review.repo_full_name, diff --git a/apps/web/src/lib/gastown/git-credentials.ts b/apps/web/src/lib/gastown/git-credentials.ts index 0f439785c1..ff01698070 100644 --- a/apps/web/src/lib/gastown/git-credentials.ts +++ b/apps/web/src/lib/gastown/git-credentials.ts @@ -38,7 +38,8 @@ export type ResolvedGitCredentials = { * Returns null if the integration is missing, inactive, or has no usable credentials. */ export async function resolveGitCredentialsFromIntegration( - platformIntegrationId: string + platformIntegrationId: string, + actorUserId: string ): Promise { const [integration] = await db .select() @@ -77,7 +78,7 @@ export async function resolveGitCredentialsFromIntegration( if (integration.platform === 'gitlab') { try { - const token = await getValidGitLabToken(integration); + const token = await getValidGitLabToken(integration, { userId: actorUserId }); const metadata = integration.metadata as GitLabMetadata | null; const instanceUrl = metadata?.gitlab_instance_url; @@ -169,8 +170,9 @@ function detectPlatformFromGitUrl(gitUrl: string): string | undefined { * meaning the existing town config credentials should be used as-is. */ export async function refreshGitCredentials( - platformIntegrationId: string | undefined | null + platformIntegrationId: string | undefined | null, + actorUserId: string ): Promise { if (!platformIntegrationId) return null; - return resolveGitCredentialsFromIntegration(platformIntegrationId); + return resolveGitCredentialsFromIntegration(platformIntegrationId, actorUserId); } diff --git a/apps/web/src/lib/integrations/gitlab-service.credentials.test.ts b/apps/web/src/lib/integrations/gitlab-service.credentials.test.ts new file mode 100644 index 0000000000..6dff58065c --- /dev/null +++ b/apps/web/src/lib/integrations/gitlab-service.credentials.test.ts @@ -0,0 +1,968 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ +import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { generateKeyPairSync } from 'node:crypto'; +import { + kilocode_users, + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import type { PlatformIntegration } from '@kilocode/db/schema'; +import { db } from '@/lib/drizzle'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import { and, eq, isNull } from 'drizzle-orm'; +import { decryptKeyedEnvelope } from '@kilocode/encryption'; +import { + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, +} from '@kilocode/worker-utils/gitlab-credential'; +import type * as GitLabAdapter from '@/lib/integrations/platforms/gitlab/adapter'; +import type * as GitLabService from './gitlab-service'; +import type { fetchGitLabCredential as FetchGitLabCredential } from '@/lib/integrations/platforms/gitlab/credential-broker-client'; + +const mockValidatePersonalAccessToken = jest.fn(); +const mockFetchGitLabProjects = jest.fn(); +const mockCreateProjectAccessToken = jest.fn(); +const mockRotateProjectAccessToken = jest.fn(); +const mockValidateProjectAccessToken = jest.fn(); +const mockRevokeProjectAccessToken = jest.fn(); +const mockFetchGitLabCredential = jest.fn(); +const mockResetCodeReviewConfigForOwner = jest.fn<() => Promise>(); + +jest.mock('@/lib/integrations/platforms/gitlab/adapter', () => ({ + ...jest.requireActual('@/lib/integrations/platforms/gitlab/adapter'), + validatePersonalAccessToken: mockValidatePersonalAccessToken, + fetchGitLabProjects: mockFetchGitLabProjects, + createProjectAccessToken: mockCreateProjectAccessToken, + rotateProjectAccessToken: mockRotateProjectAccessToken, + validateProjectAccessToken: mockValidateProjectAccessToken, + revokeProjectAccessToken: mockRevokeProjectAccessToken, +})); + +jest.mock('@/lib/agent-config/db/agent-configs', () => ({ + resetCodeReviewConfigForOwner: mockResetCodeReviewConfigForOwner, +})); + +jest.mock('@/lib/integrations/platforms/gitlab/credential-broker-client', () => ({ + fetchGitLabCredential: mockFetchGitLabCredential, +})); + +const KEY_ID = 'gitlab-service-test-key'; +const keyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}); + +jest.mock('@/lib/config.server', () => ({ + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: KEY_ID, + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: Buffer.from(keyPair.publicKey).toString('base64'), +})); + +let connectWithPAT: typeof GitLabService.connectWithPAT; +let getValidGitLabToken: typeof GitLabService.getValidGitLabToken; +let getValidGitLabProjectAccessToken: typeof GitLabService.getValidGitLabProjectAccessToken; +let getOrCreateProjectAccessToken: typeof GitLabService.getOrCreateProjectAccessToken; +let removeStoredProjectAccessToken: typeof GitLabService.removeStoredProjectAccessToken; +let disconnectGitLabIntegration: typeof GitLabService.disconnectGitLabIntegration; + +const validationResult = { + valid: true, + user: { + id: 123, + username: 'gitlab-user', + name: 'GitLab User', + email: 'gitlab-user@example.com', + avatar_url: 'https://gitlab.com/avatar.png', + web_url: 'https://gitlab.com/gitlab-user', + }, + tokenInfo: { + id: 456, + name: 'Kilo PAT', + scopes: ['api'], + expiresAt: '2027-07-13', + active: true, + lastUsedAt: null, + }, +}; + +describe('GitLab encrypted credential persistence', () => { + beforeAll(async () => { + ({ + connectWithPAT, + getValidGitLabToken, + getValidGitLabProjectAccessToken, + getOrCreateProjectAccessToken, + removeStoredProjectAccessToken, + disconnectGitLabIntegration, + } = await import('./gitlab-service')); + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockValidatePersonalAccessToken.mockResolvedValue(validationResult); + mockFetchGitLabProjects.mockResolvedValue([]); + mockCreateProjectAccessToken.mockResolvedValue({ + id: 777, + name: 'Kilo Code Review Bot', + token: 'glpat-created-project-token', + expires_at: '2027-07-13', + scopes: ['api', 'self_rotate'], + access_level: 30, + active: true, + revoked: false, + created_at: '2026-07-13T00:00:00.000Z', + last_used_at: null, + user_id: 123, + }); + mockRotateProjectAccessToken.mockResolvedValue({ + id: 778, + name: 'Kilo Code Review Bot', + token: 'glpat-rotated-project-token', + expires_at: '2027-07-13', + scopes: ['api', 'self_rotate'], + access_level: 30, + active: true, + revoked: false, + created_at: '2026-07-13T00:00:00.000Z', + last_used_at: null, + user_id: 123, + }); + mockValidateProjectAccessToken.mockResolvedValue(true); + mockRevokeProjectAccessToken.mockResolvedValue(); + mockFetchGitLabCredential.mockImplementation(async (_actor, selector) => ({ + status: 'available' as const, + token: + selector.credential === 'integration' + ? 'glpat-primary-token' + : 'glpat-created-project-token', + instanceUrl: 'https://gitlab.com', + glabIsOAuth2: false, + })); + mockResetCodeReviewConfigForOwner.mockResolvedValue(); + }); + + afterEach(async () => { + await db.delete(platform_oauth_credentials); + await db.delete(platform_access_token_credentials); + await db.delete(platform_integrations); + await db.delete(kilocode_users); + }); + + it('stores a new personal PAT only in its encrypted row with supplier evidence', async () => { + const user = await insertTestUser(); + const plaintextToken = 'glpat-new-personal-token'; + + const result = await connectWithPAT( + { type: 'user', id: user.id }, + plaintextToken, + 'https://GitLab.COM/', + user.id + ); + + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, result.integration.id)); + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, result.integration.id)); + + expect(integration).toEqual( + expect.objectContaining({ + owned_by_user_id: user.id, + integration_type: 'pat', + metadata: expect.objectContaining({ + gitlab_instance_url: 'https://gitlab.com', + auth_type: 'pat', + }), + }) + ); + expect(integration.metadata).not.toHaveProperty('access_token'); + expect(credential).toEqual( + expect.objectContaining({ + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: 'https://gitlab.com', + authorized_by_user_id: user.id, + provider_metadata: { + providerCredentialId: '456', + expiresOn: '2027-07-13', + }, + provider_scopes: ['api'], + credential_version: 1, + provider_verified_at: expect.any(String), + last_validated_at: expect.any(String), + }) + ); + expect(credential.token_encrypted).not.toContain(plaintextToken); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId: KEY_ID, privateKeyPem: keyPair.privateKey } }, + buildGitLabPersonalAccessTokenAad({ + credentialId: credential.id, + integrationId: integration.id, + providerBaseUrl: 'https://gitlab.com', + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + credentialVersion: 1, + }) + ) + ).toBe(plaintextToken); + expect( + await db + .select() + .from(platform_access_token_credentials) + .where(isNull(platform_access_token_credentials.provider_resource_id)) + ).toHaveLength(1); + }); + + it('stores PAT and project tokens only in encrypted rows', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + const credentials = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integration.id) + ); + + expect(updatedIntegration.metadata).not.toHaveProperty('access_token'); + expect(updatedIntegration.metadata).not.toHaveProperty('project_tokens'); + expect(credentials).toEqual([ + expect.objectContaining({ + provider_credential_type: 'personal_access_token', + token_encrypted: expect.any(String), + }), + expect.objectContaining({ + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + token_encrypted: expect.any(String), + }), + ]); + }); + + it('reuses an exact legacy project credential when backfill has not inserted its row', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + const legacyProjectToken = { + token_id: 991, + token: 'glpat-legacy-project-token', + expires_at: '2027-07-13', + created_at: '2026-07-13T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }; + await db + .update(platform_integrations) + .set({ + metadata: { + ...(integration.metadata as Record), + project_tokens: { '42': legacyProjectToken }, + }, + }) + .where(eq(platform_integrations.id, integration.id)); + const [integrationWithLegacyProject] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + mockFetchGitLabCredential.mockResolvedValueOnce({ + status: 'available', + token: legacyProjectToken.token, + instanceUrl: 'https://gitlab.com', + glabIsOAuth2: false, + }); + + await expect( + getOrCreateProjectAccessToken(integrationWithLegacyProject, 42, { userId: user.id }) + ).resolves.toBe(legacyProjectToken.token); + expect(mockCreateProjectAccessToken).not.toHaveBeenCalled(); + await expect( + db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')) + ).resolves.toEqual([]); + }); + + it('revokes and removes an exact legacy project credential', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + await db + .update(platform_integrations) + .set({ + metadata: { + ...(integration.metadata as Record), + project_tokens: { + '42': { + token_id: 991, + token: 'glpat-legacy-project-token', + expires_at: '2027-07-13', + created_at: '2026-07-13T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }, + '43': { + token_id: 992, + token: 'glpat-other-project-token', + expires_at: '2027-07-13', + created_at: '2026-07-13T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }, + }, + }, + }) + .where(eq(platform_integrations.id, integration.id)); + const [integrationWithLegacyProjects] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + + await removeStoredProjectAccessToken(integrationWithLegacyProjects, 42, { userId: user.id }); + + expect(mockRevokeProjectAccessToken).toHaveBeenCalledWith( + 'glpat-primary-token', + 42, + 991, + 'https://gitlab.com' + ); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updatedIntegration.metadata).toEqual( + expect.objectContaining({ + project_tokens: { '43': expect.objectContaining({ token_id: 992 }) }, + }) + ); + }); + + it.each([ + [ + 'integration', + (integration: PlatformIntegration, userId: string) => + getValidGitLabToken(integration, { userId }), + ], + [ + 'project', + (integration: PlatformIntegration, userId: string) => + getValidGitLabProjectAccessToken(integration, 42, { userId }), + ], + ])('rejects a %s credential resolved for a different GitLab instance', async (_kind, resolve) => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + mockFetchGitLabCredential.mockResolvedValueOnce({ + status: 'available', + token: 'must-not-be-returned', + instanceUrl: 'https://other-gitlab.example.com', + glabIsOAuth2: false, + }); + + await expect(resolve(integration, user.id)).rejects.toMatchObject({ + code: 'UNAUTHORIZED', + message: 'GitLab integration changed while resolving credentials', + }); + }); + + it('replaces a same-instance PAT in the stable row and preserves project credentials', async () => { + const user = await insertTestUser(); + const initial = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-initial-token', + 'https://gitlab.com', + user.id + ); + const [initialCredential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, initial.integration.id)); + const [initialIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, initial.integration.id)); + const legacyProjectToken = { + token_id: 999, + token: 'glpat-project-token', + expires_at: '2027-07-13', + created_at: '2026-07-13T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }; + + await db + .update(platform_integrations) + .set({ + metadata: { + ...(initialIntegration.metadata as Record), + project_tokens: { '99': legacyProjectToken }, + }, + }) + .where(eq(platform_integrations.id, initial.integration.id)); + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: initial.integration.id, + token_encrypted: 'encrypted-project-token', + provider_credential_type: 'project_access_token', + provider_resource_id: '99', + provider_base_url: 'https://gitlab.com', + provider_metadata: { + providerCredentialId: '999', + expiresOn: '2027-07-13', + }, + provider_scopes: ['api'], + }); + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: initial.integration.id, + authorized_by_user_id: user.id, + provider_subject_id: '123', + provider_subject_login: 'gitlab-user', + provider_base_url: 'https://gitlab.com', + access_token_encrypted: 'obsolete-oauth-token', + }); + + const replacementToken = 'glpat-replacement-token'; + await connectWithPAT( + { type: 'user', id: user.id }, + replacementToken, + 'https://gitlab.com/', + user.id + ); + + const [primaryCredential] = await db + .select() + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, initial.integration.id), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ); + const [projectCredential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.provider_resource_id, '99')); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, initial.integration.id)); + const oauthCredentials = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, initial.integration.id)); + + expect(primaryCredential.id).toBe(initialCredential.id); + expect(primaryCredential.credential_version).toBe(2); + expect( + decryptKeyedEnvelope( + primaryCredential.token_encrypted, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId: KEY_ID, privateKeyPem: keyPair.privateKey } }, + buildGitLabPersonalAccessTokenAad({ + credentialId: primaryCredential.id, + integrationId: initial.integration.id, + providerBaseUrl: 'https://gitlab.com', + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + credentialVersion: 2, + }) + ) + ).toBe(replacementToken); + expect(projectCredential).toBeDefined(); + expect(updatedIntegration.metadata).toEqual( + expect.objectContaining({ + project_tokens: { '99': legacyProjectToken }, + }) + ); + expect(updatedIntegration.metadata).not.toHaveProperty('access_token'); + expect(oauthCredentials).toHaveLength(0); + }); + + it('drops project credentials when a PAT reconnect changes GitLab instance', async () => { + const user = await insertTestUser(); + const initial = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-initial-token', + 'https://gitlab.com', + user.id + ); + const [initialCredential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, initial.integration.id)); + const [initialIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, initial.integration.id)); + + await db + .update(platform_integrations) + .set({ + metadata: { + ...(initialIntegration.metadata as Record), + project_tokens: { + '99': { + token_id: 999, + token: 'glpat-project-token', + expires_at: '2027-07-13', + created_at: '2026-07-13T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }, + }, + }, + }) + .where(eq(platform_integrations.id, initial.integration.id)); + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: initial.integration.id, + token_encrypted: 'encrypted-project-token', + provider_credential_type: 'project_access_token', + provider_resource_id: '99', + provider_base_url: 'https://gitlab.com', + provider_metadata: { + providerCredentialId: '999', + expiresOn: '2027-07-13', + }, + }); + + await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-other-instance-token', + 'https://self.gitlab.example/', + user.id + ); + + const credentials = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, initial.integration.id)); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, initial.integration.id)); + + expect(credentials).toHaveLength(1); + expect(credentials[0]).toEqual( + expect.objectContaining({ + id: initialCredential.id, + provider_resource_id: null, + provider_base_url: 'https://self.gitlab.example', + credential_version: 2, + }) + ); + expect(updatedIntegration.metadata).toEqual( + expect.objectContaining({ + gitlab_instance_url: 'https://self.gitlab.example', + }) + ); + expect(updatedIntegration.metadata).not.toHaveProperty('access_token'); + expect(updatedIntegration.metadata).not.toHaveProperty('project_tokens'); + }); + + it('stores a newly created project token only in its exact encrypted row', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + const token = await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + expect(token).toBe('glpat-created-project-token'); + expect(credential).toEqual( + expect.objectContaining({ + platform_integration_id: connected.integration.id, + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + provider_base_url: 'https://gitlab.com', + authorized_by_user_id: null, + provider_metadata: { + providerCredentialId: '777', + expiresOn: '2027-07-13', + }, + provider_scopes: ['api', 'self_rotate'], + credential_version: 1, + }) + ); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId: KEY_ID, privateKeyPem: keyPair.privateKey } }, + buildGitLabProjectAccessTokenAad({ + credentialId: credential.id, + integrationId: connected.integration.id, + providerBaseUrl: 'https://gitlab.com', + owner: { type: 'user', id: user.id }, + providerResourceId: '42', + credentialVersion: 1, + }) + ) + ).toBe('glpat-created-project-token'); + expect(updatedIntegration.metadata).not.toHaveProperty('project_tokens'); + }); + + it('rotates a project token in its stable encrypted row with a version bump', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + mockCreateProjectAccessToken.mockResolvedValueOnce({ + id: 777, + name: 'Kilo Code Review Bot', + token: 'glpat-expiring-project-token', + expires_at: '2026-07-14', + scopes: ['api', 'self_rotate'], + access_level: 30, + active: true, + revoked: false, + created_at: '2026-07-13T00:00:00.000Z', + last_used_at: null, + user_id: 123, + }); + await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + const [initialCredential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')); + let [integrationWithProject] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + const scrubbedMetadata = { + ...(integrationWithProject.metadata as Record), + }; + delete scrubbedMetadata.project_tokens; + await db + .update(platform_integrations) + .set({ metadata: scrubbedMetadata }) + .where(eq(platform_integrations.id, connected.integration.id)); + [integrationWithProject] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + const token = await getOrCreateProjectAccessToken(integrationWithProject, 42, { + userId: user.id, + }); + + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + expect(token).toBe('glpat-rotated-project-token'); + expect(credential).toEqual( + expect.objectContaining({ + id: initialCredential.id, + credential_version: 2, + provider_metadata: { + providerCredentialId: '778', + expiresOn: '2027-07-13', + }, + }) + ); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId: KEY_ID, privateKeyPem: keyPair.privateKey } }, + buildGitLabProjectAccessTokenAad({ + credentialId: credential.id, + integrationId: connected.integration.id, + providerBaseUrl: 'https://gitlab.com', + owner: { type: 'user', id: user.id }, + providerResourceId: '42', + credentialVersion: 2, + }) + ) + ).toBe('glpat-rotated-project-token'); + expect(updatedIntegration.metadata).not.toHaveProperty('project_tokens'); + }); + + it('deletes only the invalid project credential before replacement', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + let [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + mockCreateProjectAccessToken.mockResolvedValueOnce({ + id: 888, + name: 'Kilo Code Review Bot', + token: 'glpat-other-project-token', + expires_at: '2027-07-13', + scopes: ['api'], + access_level: 30, + active: true, + revoked: false, + created_at: '2026-07-13T00:00:00.000Z', + last_used_at: null, + user_id: 123, + }); + await getOrCreateProjectAccessToken(integration, 43, { userId: user.id }); + [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + mockValidateProjectAccessToken.mockResolvedValueOnce(false); + mockCreateProjectAccessToken.mockRejectedValueOnce(new Error('replacement failed')); + + await expect( + getOrCreateProjectAccessToken(integration, 42, { userId: user.id }) + ).rejects.toThrow('replacement failed'); + + const projectCredentials = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.provider_credential_type, 'project_access_token') + ); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + expect(projectCredentials.map(credential => credential.provider_resource_id)).toEqual(['43']); + expect(updatedIntegration.metadata).toEqual(expect.objectContaining({ project_tokens: {} })); + }); + + it('removes the exact encrypted project credential after explicit revocation', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + let [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + const scrubbedMetadata = { ...(integration.metadata as Record) }; + delete scrubbedMetadata.project_tokens; + await db + .update(platform_integrations) + .set({ metadata: scrubbedMetadata }) + .where(eq(platform_integrations.id, connected.integration.id)); + [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + await removeStoredProjectAccessToken(integration, 42, { userId: user.id }); + + const credentials = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integration.id) + ); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + expect(credentials).toHaveLength(1); + expect(credentials[0].provider_resource_id).toBeNull(); + expect(updatedIntegration.metadata).toEqual(expect.objectContaining({ project_tokens: {} })); + }); + + it('does not delete a project credential rotated during explicit revocation', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + let [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + mockRevokeProjectAccessToken.mockImplementationOnce(async () => { + await db + .update(platform_access_token_credentials) + .set({ credential_version: 2 }) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')); + }); + + await expect( + removeStoredProjectAccessToken(integration, 42, { userId: user.id }) + ).rejects.toThrow('GitLab project access token was replaced concurrently'); + + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + expect(credential.credential_version).toBe(2); + expect(updatedIntegration.metadata).not.toHaveProperty('project_tokens'); + }); + + it('does not invalidate a project credential rotated during validation', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + let [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + mockValidateProjectAccessToken.mockImplementationOnce(async () => { + await db + .update(platform_access_token_credentials) + .set({ credential_version: 2 }) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')); + return false; + }); + + await expect( + getOrCreateProjectAccessToken(integration, 42, { userId: user.id }) + ).rejects.toThrow('GitLab project access token was replaced concurrently'); + + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.provider_resource_id, '42')); + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + expect(credential.credential_version).toBe(2); + expect(updatedIntegration.metadata).not.toHaveProperty('project_tokens'); + }); + + it('disconnects the primary PAT while preserving project credentials', async () => { + const user = await insertTestUser(); + const connected = await connectWithPAT( + { type: 'user', id: user.id }, + 'glpat-primary-token', + 'https://gitlab.com', + user.id + ); + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + await getOrCreateProjectAccessToken(integration, 42, { userId: user.id }); + + await disconnectGitLabIntegration({ type: 'user', id: user.id }); + + const credentials = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integration.id) + ); + const [disconnectedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integration.id)); + + expect(credentials).toHaveLength(1); + expect(credentials[0].provider_resource_id).toBe('42'); + expect(disconnectedIntegration.integration_status).toBe('suspended'); + expect(disconnectedIntegration.metadata).not.toHaveProperty('access_token'); + expect(disconnectedIntegration.metadata).not.toHaveProperty('project_tokens'); + }); +}); diff --git a/apps/web/src/lib/integrations/gitlab-service.ts b/apps/web/src/lib/integrations/gitlab-service.ts index 793f0d3538..74342e7896 100644 --- a/apps/web/src/lib/integrations/gitlab-service.ts +++ b/apps/web/src/lib/integrations/gitlab-service.ts @@ -1,8 +1,12 @@ import 'server-only'; import { db } from '@/lib/drizzle'; import type { PlatformIntegration } from '@kilocode/db/schema'; -import { platform_integrations } from '@kilocode/db/schema'; -import { eq, and } from 'drizzle-orm'; +import { + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { eq, and, isNull } from 'drizzle-orm'; import { TRPCError } from '@trpc/server'; import { requireNumericPlatformRepositories, type Owner } from '@/lib/integrations/core/types'; import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; @@ -11,9 +15,6 @@ import { resetCodeReviewConfigForOwner } from '@/lib/agent-config/db/agent-confi import { fetchGitLabProjects, fetchGitLabBranches, - refreshGitLabOAuthToken, - isTokenExpired, - calculateTokenExpiry, createProjectAccessToken, findKiloProjectAccessToken, rotateProjectAccessToken, @@ -26,13 +27,31 @@ import { type GitLabPATValidationResult, GitLabProjectAccessTokenPermissionError, } from '@/lib/integrations/platforms/gitlab/adapter'; -import { randomBytes } from 'crypto'; +import { randomBytes, randomUUID } from 'crypto'; import { logExceptInTest } from '@/lib/utils.server'; import { DEFAULT_GITLAB_INSTANCE_URL, GitLabInstanceUrlError, normalizeGitLabInstanceUrl, } from '@/lib/integrations/platforms/gitlab/instance-url'; +import { + mutateGitLabMetadataInTransaction, + readGitLabMetadataInTransaction, +} from '@/lib/integrations/platforms/gitlab/metadata-mutation'; +import { + encryptGitLabPersonalAccessToken, + encryptGitLabProjectAccessToken, +} from '@/lib/integrations/platforms/gitlab/credential-encryption'; +import { + GitLabPersonalAccessTokenMetadataSchema, + GitLabProjectAccessTokenCredentialRowSchema, + GitLabProjectAccessTokenMetadataSchema, +} from '@kilocode/worker-utils/gitlab-credential'; +import { + fetchGitLabCredential, + type GitLabCredentialActor, + type GitLabCredentialBrokerResult, +} from '@/lib/integrations/platforms/gitlab/credential-broker-client'; /** * GitLab Integration Service @@ -62,6 +81,60 @@ export function instanceUrlChanged(existingUrl: string | undefined, newUrl: stri } } +function readOptionalMetadataString( + metadata: Readonly>, + key: string +): string | undefined { + const value = metadata[key]; + if (value === undefined) return undefined; + if (typeof value !== 'string') throw new Error(`GitLab metadata ${key} must be a string`); + return value; +} + +function requireMetadataRecord(metadata: unknown): Readonly> { + if (metadata === null) return {}; + if (typeof metadata !== 'object' || Array.isArray(metadata)) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid GitLab integration metadata' }); + } + return { ...metadata }; +} + +function copyMetadataObject( + metadata: Readonly>, + key: string +): Record { + const value = metadata[key]; + if (value === undefined) return {}; + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error(`GitLab metadata ${key} must be an object`); + } + return { ...value }; +} + +function countMetadataObjectEntries(value: unknown): number { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? Object.keys(value).length + : 0; +} + +function getGitLabIntegrationOwner(integration: PlatformIntegration): Owner { + if (integration.owned_by_user_id && !integration.owned_by_organization_id) { + return { type: 'user', id: integration.owned_by_user_id }; + } + if (integration.owned_by_organization_id && !integration.owned_by_user_id) { + return { type: 'org', id: integration.owned_by_organization_id }; + } + throw new Error('GitLab integration must have exactly one owner'); +} + +function requireGitLabProjectId(projectId: string | number): string { + const value = String(projectId); + if (!/^[1-9][0-9]*$/.test(value)) { + throw new Error('GitLab project ID must be a positive decimal'); + } + return value; +} + /** * Get GitLab integration for an owner */ @@ -81,70 +154,74 @@ export async function getGitLabIntegration(owner: Owner): Promise { - const metadata = integration.metadata as GitLabIntegrationMetadata | null; - - if (!metadata?.access_token) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'GitLab integration missing access token', - }); - } - - // PAT tokens don't expire in the same way as OAuth tokens - // They have a fixed expiration date set at creation - if (metadata.auth_type === 'pat') { - // For PAT, we can't refresh - just return the token - // The user will need to create a new PAT if it expires - return metadata.access_token; - } - - // OAuth token refresh logic - if (metadata.token_expires_at && isTokenExpired(metadata.token_expires_at)) { - if (!metadata.refresh_token) { +function requireAvailableGitLabCredential( + result: GitLabCredentialBrokerResult, + expectedInstanceUrl: string +): string { + if (result.status === 'available') { + if (result.instanceUrl !== expectedInstanceUrl) { throw new TRPCError({ code: 'UNAUTHORIZED', - message: 'GitLab token expired and no refresh token available. Please reconnect.', + message: 'GitLab integration changed while resolving credentials', }); } + return result.token; + } - const instanceUrl = normalizeInstanceUrl(metadata.gitlab_instance_url); - - const customCredentials = - metadata.client_id && metadata.client_secret - ? { clientId: metadata.client_id, clientSecret: metadata.client_secret } - : undefined; - - const newTokens = await refreshGitLabOAuthToken( - metadata.refresh_token, - instanceUrl, - customCredentials - ); - - const newExpiresAt = calculateTokenExpiry(newTokens.created_at, newTokens.expires_in); - - await db - .update(platform_integrations) - .set({ - metadata: { - ...metadata, - access_token: newTokens.access_token, - refresh_token: newTokens.refresh_token, - token_expires_at: newExpiresAt, - }, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, integration.id)); - - return newTokens.access_token; + switch (result.status) { + case 'invalid_request': + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid GitLab credential request' }); + case 'not_connected': + throw new TRPCError({ code: 'NOT_FOUND', message: 'GitLab integration not found' }); + case 'reconnect_required': + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'GitLab integration must be reconnected', + }); + case 'temporarily_unavailable': + throw new TRPCError({ + code: 'SERVICE_UNAVAILABLE', + message: 'GitLab credentials are temporarily unavailable', + }); } +} - return metadata.access_token; +export async function getValidGitLabToken( + integration: PlatformIntegration, + actor: GitLabCredentialActor +): Promise { + const metadata = requireMetadataRecord(integration.metadata); + const expectedInstanceUrl = normalizeInstanceUrl( + readOptionalMetadataString(metadata, 'gitlab_instance_url') + ); + return requireAvailableGitLabCredential( + await fetchGitLabCredential(actor, { + credential: 'integration', + integrationId: integration.id, + }), + expectedInstanceUrl + ); +} + +export async function getValidGitLabProjectAccessToken( + integration: PlatformIntegration, + projectId: string | number, + actor: GitLabCredentialActor +): Promise { + const metadata = requireMetadataRecord(integration.metadata); + const expectedInstanceUrl = normalizeInstanceUrl( + readOptionalMetadataString(metadata, 'gitlab_instance_url') + ); + return requireAvailableGitLabCredential( + await fetchGitLabCredential(actor, { + credential: 'project-exact', + integrationId: integration.id, + projectId: requireGitLabProjectId(projectId), + }), + expectedInstanceUrl + ); } /** @@ -154,6 +231,7 @@ export async function getValidGitLabToken(integration: PlatformIntegration): Pro export async function listGitLabRepositories( owner: Owner, integrationId: string, + actor: GitLabCredentialActor, forceRefresh: boolean = false ) { const ownershipCondition = @@ -183,7 +261,7 @@ 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 || !cachedRepositories?.length || !integration.repositories_synced_at) { - const accessToken = await getValidGitLabToken(integration); + const accessToken = await getValidGitLabToken(integration, actor); const metadata = integration.metadata as { gitlab_instance_url?: string } | null; const instanceUrl = normalizeInstanceUrl(metadata?.gitlab_instance_url); @@ -210,6 +288,7 @@ export async function listGitLabRepositories( export async function listGitLabBranches( owner: Owner, integrationId: string, + actor: GitLabCredentialActor, projectPath: string // e.g., "group/project" or project ID ) { const ownershipCondition = @@ -236,7 +315,7 @@ export async function listGitLabBranches( }); } - const accessToken = await getValidGitLabToken(integration); + const accessToken = await getValidGitLabToken(integration, actor); const metadata = integration.metadata as { gitlab_instance_url?: string } | null; const instanceUrl = normalizeInstanceUrl(metadata?.gitlab_instance_url); @@ -284,48 +363,45 @@ export async function disconnectGitLabIntegration(owner: Owner) { }); } - // Mark as disconnected instead of deleting - // This preserves webhook_secret, configured_webhooks, and project_tokens - // so reconnecting (via OAuth or PAT) will keep existing webhook configurations working - const existingMetadata = (integration.metadata || {}) as GitLabIntegrationMetadata; - - // Clear sensitive tokens but preserve webhook configuration - const updatedMetadata: GitLabIntegrationMetadata = { - // Clear tokens - access_token: undefined, - refresh_token: undefined, - token_expires_at: undefined, - // Preserve instance URL for reconnection - gitlab_instance_url: existingMetadata.gitlab_instance_url, - // Clear OAuth credentials - client_id: undefined, - client_secret: undefined, - // PRESERVE webhook secret so existing webhooks continue to work - webhook_secret: existingMetadata.webhook_secret, - // Clear auth type (will be set on reconnect) - auth_type: undefined, - // PRESERVE configured webhooks - configured_webhooks: existingMetadata.configured_webhooks, - // PRESERVE project tokens (they're still valid on GitLab) - project_tokens: existingMetadata.project_tokens, - }; - - await db - .update(platform_integrations) - .set({ - integration_status: INTEGRATION_STATUS.SUSPENDED, - metadata: updatedMetadata, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, integration.id)); + const updatedMetadata = await db.transaction(async tx => { + const nextMetadata = await mutateGitLabMetadataInTransaction(tx, integration.id, { + delete: [ + 'access_token', + 'refresh_token', + 'token_expires_at', + 'client_id', + 'client_secret', + 'auth_type', + ], + }); + await tx + .update(platform_integrations) + .set({ + integration_status: INTEGRATION_STATUS.SUSPENDED, + updated_at: new Date().toISOString(), + }) + .where(eq(platform_integrations.id, integration.id)); + await tx + .delete(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)); + await tx + .delete(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, integration.id), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ); + return nextMetadata; + }); logExceptInTest( '[disconnectGitLabIntegration] Integration suspended (preserved webhook config)', { integrationId: integration.id, - preservedWebhookSecret: !!existingMetadata.webhook_secret, - preservedWebhooks: Object.keys(existingMetadata.configured_webhooks || {}).length, - preservedProjectTokens: Object.keys(existingMetadata.project_tokens || {}).length, + preservedWebhookSecret: !!updatedMetadata.webhook_secret, + preservedWebhooks: countMetadataObjectEntries(updatedMetadata.configured_webhooks), + preservedProjectTokens: countMetadataObjectEntries(updatedMetadata.project_tokens), } ); @@ -366,7 +442,6 @@ export async function regenerateWebhookSecret(owner: Owner): Promise<{ webhookSe // Generate new webhook secret const newWebhookSecret = randomBytes(32).toString('hex'); - // Update the metadata with the new webhook secret const existingMetadata = (integration.metadata || {}) as Record; const updatedMetadata = { ...existingMetadata, @@ -388,14 +463,11 @@ export async function regenerateWebhookSecret(owner: Owner): Promise<{ webhookSe // Project Access Token (PrAT) Management // ============================================================================ -/** - * Stored Project Access Token metadata - * This is stored per-project in the integration metadata - */ +/** Legacy plaintext project credential retained only until backfill and scrub. */ export type StoredProjectAccessToken = { /** GitLab token ID (for rotation/revocation) */ token_id: number; - /** The actual token value (should be encrypted in production) */ + /** The token value retained only during the migration window. */ token: string; /** Expiration date in YYYY-MM-DD format */ expires_at: string; @@ -434,11 +506,96 @@ export type GitLabIntegrationMetadata = { */ const KILO_BOT_TOKEN_NAME = 'Kilo Code Review Bot'; +async function getStoredProjectCredential(integrationId: string, projectId: string) { + return db.transaction(async tx => { + const metadata = await readGitLabMetadataInTransaction(tx, integrationId); + const [row] = await tx + .select() + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, integrationId), + eq(platform_access_token_credentials.provider_credential_type, 'project_access_token'), + eq(platform_access_token_credentials.provider_resource_id, projectId) + ) + ) + .limit(1); + + if (row) { + const parsed = GitLabProjectAccessTokenCredentialRowSchema.safeParse(row); + const tokenId = parsed.success + ? Number(parsed.data.provider_metadata.providerCredentialId) + : Number.NaN; + if (!parsed.success || !Number.isSafeInteger(tokenId)) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'GitLab project credential must be recreated', + }); + } + return { + source: 'encrypted' as const, + credentialId: parsed.data.id, + credentialVersion: parsed.data.credential_version, + tokenId, + expiresAt: parsed.data.provider_metadata.expiresOn, + }; + } + + const projectTokens = metadata.project_tokens; + if (projectTokens === undefined) return null; + if ( + typeof projectTokens !== 'object' || + projectTokens === null || + Array.isArray(projectTokens) + ) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'GitLab project credential must be recreated', + }); + } + const candidate = (projectTokens as Record)[projectId]; + if (candidate === undefined) return null; + if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'GitLab project credential must be recreated', + }); + } + const legacy = candidate as Record; + const keys = Object.keys(legacy).sort(); + const expectedKeys = ['created_at', 'expires_at', 'name', 'token', 'token_id']; + if ( + keys.length !== expectedKeys.length || + keys.some((key, index) => key !== expectedKeys[index]) || + !Number.isSafeInteger(legacy.token_id) || + Number(legacy.token_id) <= 0 || + typeof legacy.token !== 'string' || + legacy.token.length === 0 || + typeof legacy.expires_at !== 'string' || + !/^\d{4}-\d{2}-\d{2}$/.test(legacy.expires_at) || + typeof legacy.created_at !== 'string' || + legacy.created_at.length === 0 || + typeof legacy.name !== 'string' || + legacy.name.length === 0 + ) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'GitLab project credential must be recreated', + }); + } + return { + source: 'legacy' as const, + tokenId: Number(legacy.token_id), + expiresAt: legacy.expires_at, + }; + }); +} + /** * Gets or creates a Project Access Token for a GitLab project * * This function: - * 1. Checks if a PrAT already exists for the project in metadata + * 1. Checks if an encrypted PrAT credential already exists for the project * 2. If exists and not expiring soon, returns it * 3. If exists but expiring soon, rotates it * 4. If doesn't exist, creates a new one @@ -449,66 +606,61 @@ const KILO_BOT_TOKEN_NAME = 'Kilo Code Review Bot'; */ export async function getOrCreateProjectAccessToken( integration: PlatformIntegration, - projectId: string | number + projectId: string | number, + actor: GitLabCredentialActor ): Promise { const metadata = integration.metadata as GitLabIntegrationMetadata | null; + const instanceUrl = normalizeInstanceUrl(metadata?.gitlab_instance_url); + const projectIdStr = requireGitLabProjectId(projectId); - if (!metadata?.access_token) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'GitLab integration missing access token', - }); - } - - const instanceUrl = normalizeInstanceUrl(metadata.gitlab_instance_url); - const projectIdStr = String(projectId); - - // Check if we already have a stored token for this project - const storedToken = metadata.project_tokens?.[projectIdStr]; + // Credential metadata is authoritative; the plaintext project map is migration-only. + const storedCredential = await getStoredProjectCredential(integration.id, projectIdStr); - if (storedToken) { + if (storedCredential) { // Check if token is expiring soon (within 7 days) - const isExpiringSoon = isProjectAccessTokenExpiringSoon(storedToken.expires_at, 7); + const isExpiringSoon = isProjectAccessTokenExpiringSoon(storedCredential.expiresAt, 7); if (!isExpiringSoon) { // Validate the token is still valid on GitLab (might have been manually revoked) - const isValid = await validateProjectAccessToken(storedToken.token, instanceUrl); + const projectToken = await getValidGitLabProjectAccessToken(integration, projectIdStr, actor); + const isValid = await validateProjectAccessToken(projectToken, instanceUrl); if (isValid) { logExceptInTest('[getOrCreateProjectAccessToken] Using existing token', { projectId, - tokenId: storedToken.token_id, - expiresAt: storedToken.expires_at, + tokenId: storedCredential.tokenId, + expiresAt: storedCredential.expiresAt, }); - return storedToken.token; + return projectToken; } // Token is invalid (revoked), remove from storage and create a new one logExceptInTest('[getOrCreateProjectAccessToken] Stored token is invalid, creating new one', { projectId, - tokenId: storedToken.token_id, + tokenId: storedCredential.tokenId, }); // Remove the invalid token from storage and skip to creating a new one - await removeInvalidStoredToken(integration.id, projectIdStr, metadata); + await removeInvalidStoredToken(integration.id, projectIdStr, storedCredential); // Don't try to rotate - fall through to create new token below } else { // Token is expiring soon, try to rotate it logExceptInTest('[getOrCreateProjectAccessToken] Token expiring soon, rotating', { projectId, - tokenId: storedToken.token_id, - expiresAt: storedToken.expires_at, + tokenId: storedCredential.tokenId, + expiresAt: storedCredential.expiresAt, }); + let rotatedToken: GitLabProjectAccessToken | null = null; try { // Get a valid user token for the rotation API call - const userToken = await getValidGitLabToken(integration); + const userToken = await getValidGitLabToken(integration, actor); const newExpiresAt = calculateProjectAccessTokenExpiry(365); - const rotatedToken = await rotateProjectAccessToken( + rotatedToken = await rotateProjectAccessToken( userToken, projectId, - storedToken.token_id, + storedCredential.tokenId, newExpiresAt, instanceUrl ); @@ -520,15 +672,17 @@ export async function getOrCreateProjectAccessToken( message: 'GitLab did not return token value after rotation', }); } - - // Update stored token - await updateStoredProjectAccessToken(integration.id, projectIdStr, { - token_id: rotatedToken.id, - token: rotatedToken.token, - expires_at: rotatedToken.expires_at, - created_at: new Date().toISOString(), - name: rotatedToken.name, + } catch (error) { + // If rotation fails, try to create a new token + logExceptInTest('[getOrCreateProjectAccessToken] Rotation failed, creating new token', { + projectId, + error: error instanceof Error ? error.message : String(error), }); + } + + if (rotatedToken?.token) { + // Keep persistence outside the provider fallback so a version conflict fails closed. + await updateStoredProjectAccessToken(integration, projectIdStr, rotatedToken); logExceptInTest('[getOrCreateProjectAccessToken] Token rotated successfully', { projectId, @@ -537,12 +691,6 @@ export async function getOrCreateProjectAccessToken( }); return rotatedToken.token; - } catch (error) { - // If rotation fails, try to create a new token - logExceptInTest('[getOrCreateProjectAccessToken] Rotation failed, creating new token', { - projectId, - error: error instanceof Error ? error.message : String(error), - }); } } } @@ -552,7 +700,7 @@ export async function getOrCreateProjectAccessToken( projectId, }); - const userToken = await getValidGitLabToken(integration); + const userToken = await getValidGitLabToken(integration, actor); const expiresAt = calculateProjectAccessTokenExpiry(365); try { @@ -575,13 +723,7 @@ export async function getOrCreateProjectAccessToken( } // Store the new token - await updateStoredProjectAccessToken(integration.id, projectIdStr, { - token_id: newToken.id, - token: newToken.token, - expires_at: newToken.expires_at, - created_at: new Date().toISOString(), - name: newToken.name, - }); + await updateStoredProjectAccessToken(integration, projectIdStr, newToken); logExceptInTest('[getOrCreateProjectAccessToken] Token created successfully', { projectId, @@ -609,21 +751,31 @@ export async function getOrCreateProjectAccessToken( async function removeInvalidStoredToken( integrationId: string, projectId: string, - metadata: GitLabIntegrationMetadata + credential: + | { source: 'encrypted'; credentialId: string; credentialVersion: number } + | { source: 'legacy' } ): Promise { - const projectTokens = { ...metadata.project_tokens }; - delete projectTokens[projectId]; - - await db - .update(platform_integrations) - .set({ - metadata: { - ...metadata, - project_tokens: projectTokens, - }, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, integrationId)); + await db.transaction(async tx => { + await mutateGitLabMetadataInTransaction(tx, integrationId, currentMetadata => { + const projectTokens = copyMetadataObject(currentMetadata, 'project_tokens'); + delete projectTokens[projectId]; + return { set: { project_tokens: projectTokens } }; + }); + if (credential.source === 'encrypted') { + const deleted = await tx + .delete(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.id, credential.credentialId), + eq(platform_access_token_credentials.credential_version, credential.credentialVersion) + ) + ) + .returning({ id: platform_access_token_credentials.id }); + if (deleted.length !== 1) { + throw new Error('GitLab project access token was replaced concurrently'); + } + } + }); logExceptInTest('[removeInvalidStoredToken] Removed invalid token from storage', { integrationId, @@ -632,44 +784,111 @@ async function removeInvalidStoredToken( } /** - * Updates the stored Project Access Token for a project in the integration metadata + * Stores only the exact encrypted project credential. */ async function updateStoredProjectAccessToken( - integrationId: string, + integration: PlatformIntegration, projectId: string, - tokenData: StoredProjectAccessToken + providerToken: GitLabProjectAccessToken ): Promise { - // Get current metadata - const [integration] = await db - .select() - .from(platform_integrations) - .where(eq(platform_integrations.id, integrationId)) - .limit(1); - - if (!integration) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Integration not found', - }); + if (!providerToken.token) { + throw new Error('GitLab project access token value is required'); + } + const token = providerToken.token; + const integrationType = integration.integration_type; + if (integrationType !== 'oauth' && integrationType !== 'pat') { + throw new Error('GitLab integration type must be OAuth or PAT'); } - const metadata = (integration.metadata || {}) as GitLabIntegrationMetadata; - const projectTokens = metadata.project_tokens || {}; + const owner = getGitLabIntegrationOwner(integration); + const metadata = integration.metadata as GitLabIntegrationMetadata | null; + const providerBaseUrl = normalizeInstanceUrl(metadata?.gitlab_instance_url); + const providerMetadata = GitLabProjectAccessTokenMetadataSchema.parse({ + providerCredentialId: String(providerToken.id), + expiresOn: providerToken.expires_at, + }); + const validatedAt = new Date().toISOString(); - // Update the token for this project - projectTokens[projectId] = tokenData; + await db.transaction(async tx => { + const currentMetadata = await readGitLabMetadataInTransaction(tx, integration.id); + const currentProviderBaseUrl = normalizeInstanceUrl( + readOptionalMetadataString(currentMetadata, 'gitlab_instance_url') + ); + const currentIntegrationType = readOptionalMetadataString(currentMetadata, 'auth_type'); + if (currentProviderBaseUrl !== providerBaseUrl || currentIntegrationType !== integrationType) { + throw new Error('GitLab integration changed while storing project credential'); + } + const [existingCredential] = await tx + .select() + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, integration.id), + eq(platform_access_token_credentials.provider_credential_type, 'project_access_token'), + eq(platform_access_token_credentials.provider_resource_id, projectId) + ) + ) + .limit(1); + const credentialId = existingCredential?.id ?? randomUUID(); + const credentialVersion = (existingCredential?.credential_version ?? 0) + 1; + const tokenEncrypted = encryptGitLabProjectAccessToken({ + token, + credentialId, + integrationId: integration.id, + providerBaseUrl, + owner, + providerResourceId: projectId, + credentialVersion, + }); - // Save back to database - await db - .update(platform_integrations) - .set({ - metadata: { - ...metadata, - project_tokens: projectTokens, - }, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, integrationId)); + if (existingCredential) { + const updatedCredential = await tx + .update(platform_access_token_credentials) + .set({ + token_encrypted: tokenEncrypted, + expires_at: null, + provider_credential_type: 'project_access_token', + provider_resource_id: projectId, + provider_base_url: providerBaseUrl, + authorized_by_user_id: null, + provider_metadata: providerMetadata, + provider_scopes: providerToken.scopes, + provider_verified_at: validatedAt, + credential_version: credentialVersion, + last_validated_at: validatedAt, + }) + .where( + and( + eq(platform_access_token_credentials.id, existingCredential.id), + eq( + platform_access_token_credentials.credential_version, + existingCredential.credential_version + ) + ) + ) + .returning({ id: platform_access_token_credentials.id }); + if (updatedCredential.length !== 1) { + throw new Error('GitLab project access token was replaced concurrently'); + } + return; + } + + await tx.insert(platform_access_token_credentials).values({ + id: credentialId, + platform_integration_id: integration.id, + token_encrypted: tokenEncrypted, + expires_at: null, + provider_credential_type: 'project_access_token', + provider_resource_id: projectId, + provider_base_url: providerBaseUrl, + authorized_by_user_id: null, + provider_metadata: providerMetadata, + provider_scopes: providerToken.scopes, + provider_verified_at: validatedAt, + credential_version: credentialVersion, + last_validated_at: validatedAt, + }); + }); } /** @@ -678,106 +897,84 @@ async function updateStoredProjectAccessToken( */ export async function removeStoredProjectAccessToken( integration: PlatformIntegration, - projectId: string | number + projectId: string | number, + actor: GitLabCredentialActor ): Promise { const metadata = integration.metadata as GitLabIntegrationMetadata | null; - const projectIdStr = String(projectId); - - if (!metadata?.project_tokens?.[projectIdStr]) { - // No token stored, nothing to do - return; - } - - const storedToken = metadata.project_tokens[projectIdStr]; - const instanceUrl = normalizeInstanceUrl(metadata.gitlab_instance_url); + const projectIdStr = requireGitLabProjectId(projectId); + const storedCredential = await getStoredProjectCredential(integration.id, projectIdStr); + if (!storedCredential) return; + const instanceUrl = normalizeInstanceUrl(metadata?.gitlab_instance_url); // Try to revoke the token in GitLab try { - const userToken = await getValidGitLabToken(integration); - await revokeProjectAccessToken(userToken, projectId, storedToken.token_id, instanceUrl); + const userToken = await getValidGitLabToken(integration, actor); + await revokeProjectAccessToken(userToken, projectId, storedCredential.tokenId, instanceUrl); logExceptInTest('[removeStoredProjectAccessToken] Token revoked in GitLab', { projectId, - tokenId: storedToken.token_id, + tokenId: storedCredential.tokenId, }); } catch (error) { // Log but don't fail - the token might already be revoked logExceptInTest('[removeStoredProjectAccessToken] Failed to revoke token in GitLab', { projectId, - tokenId: storedToken.token_id, + tokenId: storedCredential.tokenId, error: error instanceof Error ? error.message : String(error), }); } - // Remove from metadata - const projectTokens = { ...metadata.project_tokens }; - delete projectTokens[projectIdStr]; - - await db - .update(platform_integrations) - .set({ - metadata: { - ...metadata, - project_tokens: projectTokens, - }, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, integration.id)); + await db.transaction(async tx => { + await mutateGitLabMetadataInTransaction(tx, integration.id, currentMetadata => { + const projectTokens = copyMetadataObject(currentMetadata, 'project_tokens'); + delete projectTokens[projectIdStr]; + return { set: { project_tokens: projectTokens } }; + }); + if (storedCredential.source === 'encrypted') { + const deleted = await tx + .delete(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.id, storedCredential.credentialId), + eq( + platform_access_token_credentials.credential_version, + storedCredential.credentialVersion + ) + ) + ) + .returning({ id: platform_access_token_credentials.id }); + if (deleted.length !== 1) { + throw new Error('GitLab project access token was replaced concurrently'); + } + } else { + await tx + .delete(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, integration.id), + eq(platform_access_token_credentials.provider_credential_type, 'project_access_token'), + eq(platform_access_token_credentials.provider_resource_id, projectIdStr) + ) + ); + } + }); logExceptInTest('[removeStoredProjectAccessToken] Token removed from metadata', { projectId, }); } -/** - * Gets the stored Project Access Token for a project (if exists) - * Returns null if no token is stored - */ -export function getStoredProjectAccessToken( - integration: PlatformIntegration, - projectId: string | number -): StoredProjectAccessToken | null { - const metadata = integration.metadata as GitLabIntegrationMetadata | null; - const projectIdStr = String(projectId); - - return metadata?.project_tokens?.[projectIdStr] || null; -} - -/** - * Checks if a Project Access Token exists and is valid for a project - */ -export function hasValidProjectAccessToken( - integration: PlatformIntegration, - projectId: string | number -): boolean { - const storedToken = getStoredProjectAccessToken(integration, projectId); - - if (!storedToken) { - return false; - } - - // Check if token is not expiring within 1 day - return !isProjectAccessTokenExpiringSoon(storedToken.expires_at, 1); -} - /** * Finds an existing Kilo bot token on GitLab and imports it into metadata * Useful for recovering from lost metadata or migrating existing tokens */ export async function importExistingProjectAccessToken( integration: PlatformIntegration, - projectId: string | number + projectId: string | number, + actor: GitLabCredentialActor ): Promise { const metadata = integration.metadata as GitLabIntegrationMetadata | null; - - if (!metadata?.access_token) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'GitLab integration missing access token', - }); - } - - const instanceUrl = normalizeInstanceUrl(metadata.gitlab_instance_url); - const userToken = await getValidGitLabToken(integration); + const instanceUrl = normalizeInstanceUrl(metadata?.gitlab_instance_url); + const userToken = await getValidGitLabToken(integration, actor); // Find existing Kilo token on GitLab const existingToken = await findKiloProjectAccessToken( @@ -830,11 +1027,13 @@ export { validatePersonalAccessToken, type GitLabPATValidationResult }; * @param owner - User or organization owner * @param token - Personal Access Token * @param instanceUrl - GitLab instance URL + * @param authorizedByUserId - Authenticated Kilo user who supplied the PAT */ export async function connectWithPAT( owner: Owner, token: string, - instanceUrl: string = 'https://gitlab.com' + instanceUrl: string = 'https://gitlab.com', + authorizedByUserId: string ): Promise<{ success: boolean; integration: { @@ -865,54 +1064,149 @@ export async function connectWithPAT( message: validation.error || 'Invalid Personal Access Token', }); } + const validatedUser = validation.user; + const validatedAt = new Date().toISOString(); + const providerMetadata = GitLabPersonalAccessTokenMetadataSchema.parse({ + providerCredentialId: + validation.tokenInfo?.id === undefined ? undefined : String(validation.tokenInfo.id), + expiresOn: validation.tokenInfo?.expiresAt ?? undefined, + }); // 2. Check for existing integration - update it instead of creating new const existingIntegration = await getGitLabIntegration(owner); if (existingIntegration) { - const existingMetadata = (existingIntegration.metadata || {}) as GitLabIntegrationMetadata; + let existingMetadata: Record = {}; + let isInstanceChange = false; + + await db.transaction(async tx => { + existingMetadata = await readGitLabMetadataInTransaction(tx, existingIntegration.id); + isInstanceChange = instanceUrlChanged( + readOptionalMetadataString(existingMetadata, 'gitlab_instance_url'), + normalizedInstanceUrl + ); + const [existingPrimaryCredential] = await tx + .select() + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, existingIntegration.id), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ) + .limit(1); + const credentialId = existingPrimaryCredential?.id ?? randomUUID(); + const credentialVersion = (existingPrimaryCredential?.credential_version ?? 0) + 1; + const tokenEncrypted = encryptGitLabPersonalAccessToken({ + token, + credentialId, + integrationId: existingIntegration.id, + providerBaseUrl: normalizedInstanceUrl, + owner, + authorizedByUserId, + credentialVersion, + }); - // Detect if the GitLab instance URL changed (e.g. gitlab.com → self-hosted) - const isInstanceChange = instanceUrlChanged( - existingMetadata.gitlab_instance_url, - normalizedInstanceUrl - ); + await mutateGitLabMetadataInTransaction(tx, existingIntegration.id, { + set: { + gitlab_instance_url: normalizedInstanceUrl, + auth_type: 'pat', + webhook_secret: isInstanceChange + ? randomBytes(32).toString('hex') + : (readOptionalMetadataString(existingMetadata, 'webhook_secret') ?? + randomBytes(32).toString('hex')), + }, + delete: [ + 'access_token', + 'refresh_token', + 'token_expires_at', + 'client_id', + 'client_secret', + ...(isInstanceChange ? ['configured_webhooks', 'project_tokens'] : []), + ], + }); + + await tx + .update(platform_integrations) + .set({ + integration_type: 'pat', + platform_installation_id: String(validatedUser.id), + platform_account_id: String(validatedUser.id), + platform_account_login: validatedUser.username, + scopes: validation.tokenInfo?.scopes ?? ['api'], + integration_status: INTEGRATION_STATUS.ACTIVE, + updated_at: new Date().toISOString(), + }) + .where(eq(platform_integrations.id, existingIntegration.id)); + + if (isInstanceChange) { + await tx + .delete(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, existingIntegration.id) + ); + } + + if (existingPrimaryCredential && !isInstanceChange) { + const updatedCredential = await tx + .update(platform_access_token_credentials) + .set({ + token_encrypted: tokenEncrypted, + expires_at: null, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: normalizedInstanceUrl, + authorized_by_user_id: authorizedByUserId, + provider_metadata: providerMetadata, + provider_scopes: validation.tokenInfo?.scopes ?? null, + provider_verified_at: validatedAt, + credential_version: credentialVersion, + last_validated_at: validatedAt, + }) + .where( + and( + eq(platform_access_token_credentials.id, existingPrimaryCredential.id), + eq( + platform_access_token_credentials.credential_version, + existingPrimaryCredential.credential_version + ) + ) + ) + .returning({ id: platform_access_token_credentials.id }); + if (updatedCredential.length !== 1) { + throw new Error('GitLab PAT was replaced concurrently'); + } + } else { + await tx.insert(platform_access_token_credentials).values({ + id: credentialId, + platform_integration_id: existingIntegration.id, + token_encrypted: tokenEncrypted, + expires_at: null, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: normalizedInstanceUrl, + authorized_by_user_id: authorizedByUserId, + provider_metadata: providerMetadata, + provider_scopes: validation.tokenInfo?.scopes ?? null, + provider_verified_at: validatedAt, + credential_version: credentialVersion, + last_validated_at: validatedAt, + }); + } + + await tx + .delete(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, existingIntegration.id)); + }); if (isInstanceChange) { logExceptInTest('[connectWithPAT] Instance URL changed — clearing stale config', { integrationId: existingIntegration.id, - oldInstanceUrl: existingMetadata.gitlab_instance_url, + oldInstanceUrl: readOptionalMetadataString(existingMetadata, 'gitlab_instance_url'), newInstanceUrl: normalizedInstanceUrl, }); } - const updatedMetadata: GitLabIntegrationMetadata = { - access_token: token, - gitlab_instance_url: normalizedInstanceUrl, - auth_type: 'pat', - // If instance changed: generate fresh webhook secret, clear webhooks & tokens - // If same instance: preserve existing config for continuity - webhook_secret: isInstanceChange - ? randomBytes(32).toString('hex') - : existingMetadata.webhook_secret || randomBytes(32).toString('hex'), - configured_webhooks: isInstanceChange ? undefined : existingMetadata.configured_webhooks, - project_tokens: isInstanceChange ? undefined : existingMetadata.project_tokens, - }; - - await db - .update(platform_integrations) - .set({ - integration_type: 'pat', - platform_installation_id: String(validation.user.id), - platform_account_id: String(validation.user.id), - platform_account_login: validation.user.username, - scopes: validation.tokenInfo?.scopes ?? ['api'], - integration_status: INTEGRATION_STATUS.ACTIVE, - metadata: updatedMetadata, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, existingIntegration.id)); - // If instance changed, reset the code review agent config // (selected repos and manually added repos belong to the old instance) if (isInstanceChange) { @@ -921,15 +1215,16 @@ export async function connectWithPAT( logExceptInTest('[connectWithPAT] Integration updated', { integrationId: existingIntegration.id, - userId: validation.user.id, - username: validation.user.username, + userId: validatedUser.id, + username: validatedUser.username, instanceUrl: normalizedInstanceUrl, authType: 'pat', instanceChanged: isInstanceChange, - preservedWebhookSecret: !isInstanceChange && !!existingMetadata.webhook_secret, + preservedWebhookSecret: + !isInstanceChange && !!readOptionalMetadataString(existingMetadata, 'webhook_secret'), preservedWebhooks: isInstanceChange ? 0 - : Object.keys(existingMetadata.configured_webhooks || {}).length, + : countMetadataObjectEntries(existingMetadata.configured_webhooks), }); // Fetch and cache repositories @@ -940,8 +1235,8 @@ export async function connectWithPAT( success: true, integration: { id: existingIntegration.id, - accountLogin: validation.user.username, - accountId: String(validation.user.id), + accountLogin: validatedUser.username, + accountId: String(validatedUser.id), instanceUrl: normalizedInstanceUrl, }, warnings: validation.warnings, @@ -953,37 +1248,73 @@ export async function connectWithPAT( // 4. Prepare metadata const metadata: GitLabIntegrationMetadata = { - access_token: token, // No refresh_token for PAT (PATs don't refresh) gitlab_instance_url: normalizedInstanceUrl, webhook_secret: webhookSecret, auth_type: 'pat', }; - // 5. Create integration - const [integration] = await db - .insert(platform_integrations) - .values({ - owned_by_user_id: owner.type === 'user' ? owner.id : null, - owned_by_organization_id: owner.type === 'org' ? owner.id : null, - platform: PLATFORM.GITLAB, - integration_type: 'pat', - platform_installation_id: String(validation.user.id), // Use GitLab user ID as "installation" ID - platform_account_id: String(validation.user.id), - platform_account_login: validation.user.username, - permissions: null, // PAT doesn't have granular permissions like GitHub Apps - scopes: validation.tokenInfo?.scopes ?? ['api'], - repository_access: 'all', // PAT grants access to all user's projects - integration_status: INTEGRATION_STATUS.ACTIVE, - metadata, - installed_at: new Date().toISOString(), - }) - .returning(); + const integrationId = randomUUID(); + const credentialId = randomUUID(); + const credentialVersion = 1; + const tokenEncrypted = encryptGitLabPersonalAccessToken({ + token, + credentialId, + integrationId, + providerBaseUrl: normalizedInstanceUrl, + owner, + authorizedByUserId, + credentialVersion, + }); + + // 5. Create the parent and encrypted credential atomically. + const integration = await db.transaction(async tx => { + const [createdIntegration] = await tx + .insert(platform_integrations) + .values({ + id: integrationId, + owned_by_user_id: owner.type === 'user' ? owner.id : null, + owned_by_organization_id: owner.type === 'org' ? owner.id : null, + platform: PLATFORM.GITLAB, + integration_type: 'pat', + platform_installation_id: String(validatedUser.id), // Use GitLab user ID as "installation" ID + platform_account_id: String(validatedUser.id), + platform_account_login: validatedUser.username, + permissions: null, // PAT doesn't have granular permissions like GitHub Apps + scopes: validation.tokenInfo?.scopes ?? ['api'], + repository_access: 'all', // PAT grants access to all user's projects + integration_status: INTEGRATION_STATUS.ACTIVE, + metadata, + installed_at: validatedAt, + }) + .returning(); + + await tx.insert(platform_access_token_credentials).values({ + id: credentialId, + platform_integration_id: integrationId, + token_encrypted: tokenEncrypted, + expires_at: null, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: normalizedInstanceUrl, + authorized_by_user_id: authorizedByUserId, + provider_metadata: providerMetadata, + provider_scopes: validation.tokenInfo?.scopes ?? null, + provider_verified_at: validatedAt, + credential_version: credentialVersion, + last_validated_at: validatedAt, + }); + await tx + .delete(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integrationId)); + + return createdIntegration; + }); logExceptInTest('[connectWithPAT] Integration created', { integrationId: integration.id, - userId: validation.user.id, - username: validation.user.username, + userId: validatedUser.id, + username: validatedUser.username, instanceUrl: normalizedInstanceUrl, authType: 'pat', }); @@ -1001,8 +1332,8 @@ export async function connectWithPAT( success: true, integration: { id: integration.id, - accountLogin: validation.user.username, - accountId: String(validation.user.id), + accountLogin: validatedUser.username, + accountId: String(validatedUser.id), instanceUrl: normalizedInstanceUrl, }, warnings: validation.warnings, diff --git a/apps/web/src/lib/integrations/oauth/platforms/gitlab-callback.ts b/apps/web/src/lib/integrations/oauth/platforms/gitlab-callback.ts index 7bb32b5d5c..ded1ff3605 100644 --- a/apps/web/src/lib/integrations/oauth/platforms/gitlab-callback.ts +++ b/apps/web/src/lib/integrations/oauth/platforms/gitlab-callback.ts @@ -2,10 +2,7 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { getUserFromAuth } from '@/lib/user/server'; import { ensureOrganizationAccess } from '@/routers/organizations/utils'; -import { db } from '@/lib/drizzle'; -import { platform_integrations } from '@kilocode/db/schema'; -import { eq, and } from 'drizzle-orm'; -import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; +import { PLATFORM } from '@/lib/integrations/core/constants'; import { captureException, captureMessage } from '@sentry/nextjs'; import { exchangeGitLabOAuthCode, @@ -13,27 +10,17 @@ import { fetchGitLabProjects, calculateTokenExpiry, } from '@/lib/integrations/platforms/gitlab/adapter'; -import { instanceUrlChanged } from '@/lib/integrations/gitlab-service'; -import { - isDefaultGitLabInstanceUrl, - normalizeGitLabInstanceUrl, -} from '@/lib/integrations/platforms/gitlab/instance-url'; +import { normalizeGitLabInstanceUrl } from '@/lib/integrations/platforms/gitlab/instance-url'; import { resetCodeReviewConfigForOwner } from '@/lib/agent-config/db/agent-configs'; import { APP_URL } from '@/lib/constants'; -import { createHash, randomBytes } from 'crypto'; +import { createHash } from 'crypto'; import { type VerifiedGitLabOAuthState, verifyGitLabOAuthState, } from '@/lib/integrations/platforms/gitlab/oauth-state'; import { getGitLabOAuthCredentials } from '@/lib/integrations/platforms/gitlab/oauth-credentials'; import { appendIntegrationOAuthRedirectQuery } from '@/lib/integrations/oauth/common'; - -/** - * Generates a secure random webhook secret for GitLab webhook verification - */ -function generateWebhookSecret(): string { - return randomBytes(32).toString('hex'); -} +import { storeGitLabOAuthIntegration } from '@/lib/integrations/platforms/gitlab/oauth-integration-writer'; function buildGitLabRedirectPath( state: Pick | null | undefined, @@ -171,85 +158,22 @@ export async function handleGitLabOAuthCallback(request: NextRequest) { const tokenExpiresAt = calculateTokenExpiry(tokens.created_at, tokens.expires_in); - const ownershipCondition = - owner.type === 'user' - ? eq(platform_integrations.owned_by_user_id, owner.id) - : eq(platform_integrations.owned_by_organization_id, owner.id); - - const [existing] = await db - .select() - .from(platform_integrations) - .where(and(ownershipCondition, eq(platform_integrations.platform, PLATFORM.GITLAB))) - .limit(1); - - const existingMetadata = existing?.metadata as Record | null; - - // Detect if the GitLab instance URL changed (e.g. gitlab.com -> self-hosted) - const isInstanceChange = - existing !== undefined && - instanceUrlChanged( - existingMetadata?.gitlab_instance_url as string | undefined, - normalizedInstanceUrl - ); - - const webhookSecret = isInstanceChange - ? generateWebhookSecret() - : ((existingMetadata?.webhook_secret as string | undefined) ?? generateWebhookSecret()); - - const metadata: Record = { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - token_expires_at: tokenExpiresAt, - gitlab_instance_url: isDefaultGitLabInstanceUrl(normalizedInstanceUrl) - ? undefined - : normalizedInstanceUrl, - webhook_secret: webhookSecret, - auth_type: 'oauth', - // Only preserve webhooks/tokens if same instance - configured_webhooks: isInstanceChange ? undefined : existingMetadata?.configured_webhooks, - project_tokens: isInstanceChange ? undefined : existingMetadata?.project_tokens, - }; - - if (customCredentials) { - metadata.client_id = customCredentials.clientId; - metadata.client_secret = customCredentials.clientSecret; - } - - if (existing) { - await db - .update(platform_integrations) - .set({ - platform_account_id: gitlabUser.id.toString(), - platform_account_login: gitlabUser.username, - scopes: tokens.scope.split(' '), - integration_status: INTEGRATION_STATUS.ACTIVE, - repositories: repositories && repositories.length > 0 ? repositories : null, - metadata, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, existing.id)); + const stored = await storeGitLabOAuthIntegration({ + owner, + authorizedByUserId: user.id, + providerBaseUrl: normalizedInstanceUrl, + providerUser: { id: gitlabUser.id.toString(), login: gitlabUser.username }, + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + accessTokenExpiresAt: tokenExpiresAt, + oauthClientId: customCredentials?.clientId ?? null, + oauthClientSecret: customCredentials?.clientSecret ?? null, + scopes: tokens.scope.split(' '), + repositories: repositories && repositories.length > 0 ? repositories : null, + }); - // If instance changed, reset the code review agent config - if (isInstanceChange) { - await resetCodeReviewConfigForOwner(owner, PLATFORM.GITLAB); - } - } else { - await db.insert(platform_integrations).values({ - owned_by_user_id: owner.type === 'user' ? owner.id : null, - owned_by_organization_id: owner.type === 'org' ? owner.id : null, - platform: PLATFORM.GITLAB, - integration_type: 'oauth', - platform_installation_id: gitlabUser.id.toString(), // Use GitLab user ID as "installation" ID - platform_account_id: gitlabUser.id.toString(), - platform_account_login: gitlabUser.username, - permissions: null, // GitLab OAuth doesn't have granular permissions like GitHub Apps - scopes: tokens.scope.split(' '), - repository_access: 'all', // OAuth grants access to all user's projects - integration_status: INTEGRATION_STATUS.ACTIVE, - repositories: repositories && repositories.length > 0 ? repositories : null, - metadata, - installed_at: new Date().toISOString(), - }); + if (stored.instanceChanged) { + await resetCodeReviewConfigForOwner(owner, PLATFORM.GITLAB); } const successPath = verifiedState.returnTo diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts index ccd5ec58d4..2d41601bd3 100644 --- a/apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts +++ b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts @@ -91,7 +91,6 @@ async function insertExistingBitbucketIntegration(kiloUserId: string) { .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', @@ -172,7 +171,6 @@ describe('Bitbucket OAuth credential storage', () => { ); expect(credential).toEqual( expect.objectContaining({ - platform: 'bitbucket', authorized_by_user_id: user.id, provider_subject_id: 'bitbucket-user-uuid', provider_subject_login: 'octobucket', diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts index 878f107be1..74b637d424 100644 --- a/apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts +++ b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts @@ -139,7 +139,6 @@ export async function storeBitbucketIntegration(input: StoreBitbucketIntegration 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, diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts b/apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts index 1f0d538fa1..fad17511a4 100644 --- a/apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts +++ b/apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts @@ -99,10 +99,7 @@ async function findBitbucketOAuthIntegration(owner: Owner) { .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) - ) + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id) ) .where(oauthIntegrationCondition(owner)) .limit(1); 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 index e6963c8dbb..d680d7d7d7 100644 --- a/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.test.ts +++ b/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.test.ts @@ -111,7 +111,6 @@ async function insertActiveIntegration( 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', @@ -172,6 +171,22 @@ describe('Bitbucket repository cache', () => { expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); }); + it('fails closed for an incomplete legacy OAuth credential profile', async () => { + const integration = await insertActiveIntegration(user.id); + await db + .update(platform_oauth_credentials) + .set({ authorized_by_user_id: null }) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)); + + await expect( + listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + }) + ).resolves.toEqual({ status: 'reconnect_required' }); + expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); + }); + it('treats an empty synchronized repository list as an initialized cache', async () => { await insertActiveIntegration(user.id, { repositories: [] }); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts b/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts index 6cb9bc8755..ef76c3ffc8 100644 --- a/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts +++ b/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts @@ -3,6 +3,7 @@ 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 { BitbucketOAuthCredentialRowSchema } from '@kilocode/worker-utils/bitbucket-workspace-access-token'; import { captureException, captureMessage } from '@sentry/nextjs'; import { after } from 'next/server'; import { db } from '@/lib/drizzle'; @@ -104,16 +105,12 @@ export async function listBitbucketRepositories({ 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, + credential: platform_oauth_credentials, }) .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) - ) + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id) ) .where(and(ownerCondition(owner), eq(platform_integrations.platform, PLATFORM.BITBUCKET))) .limit(1); @@ -122,7 +119,10 @@ export async function listBitbucketRepositories({ if (expectedIntegrationId && row.integrationId !== expectedIntegrationId) { return { status: 'temporarily_unavailable' }; } - if (!row.credentialId || row.revokedAt) return { status: 'reconnect_required' }; + const credential = BitbucketOAuthCredentialRowSchema.safeParse(row.credential); + if (!credential.success || credential.data.revoked_at) { + return { status: 'reconnect_required' }; + } const metadata = BitbucketIntegrationMetadataSchema.safeParse(row.metadata); if (!metadata.success) return { status: 'reconnect_required' }; 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 index 4a60459509..611087fe38 100644 --- 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 @@ -3,6 +3,7 @@ import { generateKeyPairSync } from 'node:crypto'; import { decryptKeyedEnvelope } from '@kilocode/encryption'; import { BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + BitbucketWorkspaceAccessTokenCredentialRowSchema, buildBitbucketWorkspaceAccessTokenAad, } from '@kilocode/worker-utils/bitbucket-workspace-access-token'; import { db } from '@/lib/drizzle'; @@ -237,17 +238,15 @@ describe('Bitbucket Workspace Access Token credentials', () => { expect(credential).toEqual( expect.objectContaining({ platform_integration_id: integration.id, - owned_by_organization_id: null, - platform: null, - integration_type: null, 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 credentialProfile = BitbucketWorkspaceAccessTokenCredentialRowSchema.parse(credential); + expect(new Date(credentialProfile.provider_verified_at).toISOString()).toBe(result.validatedAt); + expect(new Date(credentialProfile.last_validated_at).toISOString()).toBe(result.validatedAt); const aad = buildBitbucketWorkspaceAccessTokenAad({ credentialId: credential.id, @@ -367,7 +366,6 @@ describe('Bitbucket Workspace Access Token credentials', () => { ); if (!connectedCredential) throw new Error('Expected connected credential'); - expect(connectedCredential.owned_by_organization_id).toBeNull(); expect( decryptKeyedEnvelope( connectedCredential.token_encrypted, @@ -601,6 +599,7 @@ describe('Bitbucket Workspace Access Token credentials', () => { .from(organization_audit_logs) .where(eq(organization_audit_logs.organization_id, organization.id)); if (!integration || !credential) throw new Error('Expected rotated integration'); + const credentialProfile = BitbucketWorkspaceAccessTokenCredentialRowSchema.parse(credential); expect(rotated).toEqual({ integrationId: connected.integrationId, @@ -624,8 +623,10 @@ describe('Bitbucket Workspace Access Token credentials', () => { '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(new Date(credentialProfile.provider_verified_at).toISOString()).toBe( + rotated.validatedAt + ); + expect(new Date(credentialProfile.last_validated_at).toISOString()).toBe(rotated.validatedAt); expect(integration).toEqual( expect.objectContaining({ auth_invalid_at: null, 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 index bb54d11926..a3c3458671 100644 --- 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 @@ -4,8 +4,8 @@ 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, BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_EFFECTIVE_SCOPES, + BitbucketWorkspaceAccessTokenCredentialRowSchema, getMissingBitbucketWorkspaceAccessTokenScopes, getUnexpectedBitbucketWorkspaceAccessTokenScopes, hasRequiredBitbucketWorkspaceAccessTokenScopes, @@ -126,12 +126,7 @@ async function loadIntegration(organizationId: string) { repositoriesSyncedAt: platform_integrations.repositories_synced_at, authInvalidAt: platform_integrations.auth_invalid_at, authInvalidReason: platform_integrations.auth_invalid_reason, - credentialId: platform_access_token_credentials.id, - 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, + credential: platform_access_token_credentials, }) .from(platform_integrations) .leftJoin( @@ -221,17 +216,20 @@ function parseIntegration(row: LoadedIntegration) { const cache = workspace ? parseCachedRepositories(row.repositories, row.repositoriesSyncedAt, workspace) : null; - const credential = - row.credentialId !== null && row.credentialVersion !== null && row.credentialVersion > 0 - ? { id: row.credentialId, version: row.credentialVersion } + const parsedCredential = BitbucketWorkspaceAccessTokenCredentialRowSchema.safeParse( + row.credential + ); + const credentialProfile = + parsedCredential.success && parsedCredential.data.platform_integration_id === row.integrationId + ? parsedCredential.data : null; + const credential = credentialProfile + ? { id: credentialProfile.id, version: credentialProfile.credential_version } + : 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; + credentialProfile !== null && + hasRequiredBitbucketWorkspaceAccessTokenScopes(credentialProfile.provider_scopes); const usable = workspace !== null && row.installationId === null && @@ -251,6 +249,7 @@ function parseIntegration(row: LoadedIntegration) { workspace, cache, credential, + credentialProfile, state: usable ? ('usable' as const) : ('reconnect_required' as const), recoveryAction: usable ? null @@ -309,9 +308,9 @@ export async function getBitbucketWorkspaceAccessTokenStatus(organizationId: str workspace: integration.workspace, invalidatedAt: toIsoTimestamp(integration.row.authInvalidAt), invalidationReason: invalidationReason.success ? invalidationReason.data : null, - lastValidatedAt: toIsoTimestamp(integration.row.lastValidatedAt), + lastValidatedAt: toIsoTimestamp(integration.credentialProfile?.last_validated_at ?? null), unexpectedScopes: getUnexpectedBitbucketWorkspaceAccessTokenScopes( - integration.row.providerScopes ?? [] + integration.credentialProfile?.provider_scopes ?? [] ), repositoryCache: integration.cache ?? { status: 'uninitialized' as const, @@ -386,7 +385,7 @@ export async function getBitbucketCodeReviewerReadiness(organizationId: string) } const missingRequiredScopes = getMissingBitbucketWorkspaceAccessTokenScopes( - integration.row.providerScopes ?? [] + integration.credentialProfile?.provider_scopes ?? [] ); const connected = integration.state === 'usable' && integration.workspace !== null; const cache = integration.cache ?? repositoryCache; @@ -394,7 +393,9 @@ export async function getBitbucketCodeReviewerReadiness(organizationId: string) connected, ready: connected && - hasRequiredBitbucketWorkspaceAccessTokenScopes(integration.row.providerScopes ?? []) && + hasRequiredBitbucketWorkspaceAccessTokenScopes( + integration.credentialProfile?.provider_scopes ?? [] + ) && cache.status === 'available', integrationId: integration.row.integrationId, workspace: integration.workspace, diff --git a/apps/web/src/lib/integrations/platforms/gitlab/adapter.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/adapter.test.ts index 308a2da409..d87ad3214b 100644 --- a/apps/web/src/lib/integrations/platforms/gitlab/adapter.test.ts +++ b/apps/web/src/lib/integrations/platforms/gitlab/adapter.test.ts @@ -12,7 +12,6 @@ import { PassThrough } from 'stream'; import { buildGitLabOAuthUrl, exchangeGitLabOAuthCode, - refreshGitLabOAuthToken, validateGitLabInstance, validatePersonalAccessToken, createProjectWebhook, @@ -153,13 +152,6 @@ describe('GitLab OAuth endpoint safety', () => { ).rejects.toThrow('Custom GitLab OAuth credentials are required for self-hosted instances'); expect(mockFetch).not.toHaveBeenCalled(); }); - - it('refuses to refresh self-hosted OAuth tokens without custom credentials', async () => { - await expect( - refreshGitLabOAuthToken('refresh-token', 'https://attacker.example') - ).rejects.toThrow('Custom GitLab OAuth credentials are required for self-hosted instances'); - expect(mockFetch).not.toHaveBeenCalled(); - }); }); describe('validateGitLabInstance', () => { diff --git a/apps/web/src/lib/integrations/platforms/gitlab/adapter.ts b/apps/web/src/lib/integrations/platforms/gitlab/adapter.ts index bafd7bcca9..cae231422d 100644 --- a/apps/web/src/lib/integrations/platforms/gitlab/adapter.ts +++ b/apps/web/src/lib/integrations/platforms/gitlab/adapter.ts @@ -403,60 +403,6 @@ export async function exchangeGitLabOAuthCode( return tokens; } -/** - * Refreshes an expired OAuth access token using the refresh token - * - * @param refreshToken - The refresh token - * @param instanceUrl - GitLab instance URL (defaults to gitlab.com) - * @param customCredentials - Optional custom OAuth credentials for self-hosted instances - */ -export async function refreshGitLabOAuthToken( - refreshToken: string, - instanceUrl: string = DEFAULT_GITLAB_URL, - customCredentials?: GitLabOAuthCredentials -): Promise { - const normalizedInstanceUrl = normalizeGitLabInstanceUrl(instanceUrl); - if (!isDefaultGitLabInstanceUrl(normalizedInstanceUrl) && !customCredentials) { - throw new Error('Custom GitLab OAuth credentials are required for self-hosted instances'); - } - - const clientId = customCredentials?.clientId || GITLAB_CLIENT_ID; - const clientSecret = customCredentials?.clientSecret || GITLAB_CLIENT_SECRET; - - if (!clientId || !clientSecret) { - throw new Error('GitLab OAuth credentials not configured'); - } - - const response = await fetchGitLab(buildGitLabUrl(normalizedInstanceUrl, '/oauth/token'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - client_id: clientId, - client_secret: clientSecret, - refresh_token: refreshToken, - grant_type: 'refresh_token', - }), - }); - - if (!response.ok) { - const error = await response.text(); - logExceptInTest('GitLab OAuth token refresh failed:', { status: response.status, error }); - throw new Error(`GitLab OAuth token refresh failed: ${response.status}`); - } - - const tokens = (await response.json()) as GitLabOAuthTokens; - - logExceptInTest('GitLab OAuth tokens refreshed', { - hasAccessToken: !!tokens.access_token, - hasRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return tokens; -} - /** * Fetches the authenticated GitLab user's information * @@ -648,21 +594,6 @@ export function calculateTokenExpiry(createdAt: number, expiresIn: number): stri return new Date(expiresAtMs).toISOString(); } -/** - * Checks if a token is expired or about to expire (within 5 minutes) - * - * @param expiresAt - ISO timestamp of token expiration - */ -export function isTokenExpired(expiresAt: string | null): boolean { - if (!expiresAt) return true; - - const expiryTime = new Date(expiresAt).getTime(); - const now = Date.now(); - const bufferMs = 5 * 60 * 1000; // 5 minutes buffer - - return now >= expiryTime - bufferMs; -} - // ============================================================================ // Webhook Verification // ============================================================================ diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-broker-client.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-broker-client.test.ts new file mode 100644 index 0000000000..18437a7258 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-broker-client.test.ts @@ -0,0 +1,149 @@ +import { + fetchGitLabCredential, + GitLabCredentialBrokerResultSchema, +} from './credential-broker-client'; + +const mockConfig = { apiUrl: 'https://git-token-service.example.com' }; +const mockGenerateInternalServiceToken = jest.fn( + (userId: string, _options: { expiresIn: number; audience: string; organizationId?: string }) => + `broker-token:${userId}` +); + +jest.mock('@/lib/config.server', () => ({ + get GIT_TOKEN_SERVICE_API_URL() { + return mockConfig.apiUrl; + }, +})); + +jest.mock('@/lib/tokens', () => ({ + TOKEN_EXPIRY: { fiveMinutes: 5 * 60 }, + generateInternalServiceToken: ( + userId: string, + options: { expiresIn: number; audience: string; organizationId?: string } + ) => mockGenerateInternalServiceToken(userId, options), +})); + +function jsonResponse(body: unknown, init?: ResponseInit): Response { + return new Response(JSON.stringify(body), { + ...init, + headers: { 'Content-Type': 'application/json', ...init?.headers }, + }); +} + +describe('fetchGitLabCredential', () => { + beforeEach(() => { + jest.restoreAllMocks(); + mockConfig.apiUrl = 'https://git-token-service.example.com'; + mockGenerateInternalServiceToken.mockClear(); + }); + + it('uses a purpose-bound actor token and posts the strict integration selector', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce( + jsonResponse({ + status: 'available', + token: 'glpat-integration-token', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: false, + }) + ); + const result = await fetchGitLabCredential( + { userId: 'user-1', organizationId: '11111111-1111-4111-8111-111111111111' }, + { credential: 'integration', integrationId: '22222222-2222-4222-8222-222222222222' } + ); + + expect(mockGenerateInternalServiceToken).toHaveBeenCalledWith('user-1', { + expiresIn: 5 * 60, + audience: 'git-token-service:gitlab-credentials', + organizationId: '11111111-1111-4111-8111-111111111111', + }); + expect(result).toEqual({ + status: 'available', + token: 'glpat-integration-token', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: false, + }); + expect(fetchMock).toHaveBeenCalledWith( + 'https://git-token-service.example.com/internal/gitlab/credentials', + expect.objectContaining({ + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: 'Bearer broker-token:user-1', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + credential: 'integration', + integrationId: '22222222-2222-4222-8222-222222222222', + }), + redirect: 'error', + signal: expect.anything(), + }) + ); + }); + + it('posts only the exact requested project selector', async () => { + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(jsonResponse({ status: 'not_connected' })); + + await fetchGitLabCredential( + { userId: 'user-1' }, + { + credential: 'project-exact', + integrationId: '22222222-2222-4222-8222-222222222222', + projectId: '42', + } + ); + + expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({ + credential: 'project-exact', + integrationId: '22222222-2222-4222-8222-222222222222', + projectId: '42', + }); + }); + + it('fails closed when service configuration is unavailable', async () => { + mockConfig.apiUrl = ''; + const fetchMock = jest.spyOn(global, 'fetch'); + + await expect( + fetchGitLabCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: '22222222-2222-4222-8222-222222222222' } + ) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it.each([ + ['an extra response field', jsonResponse({ status: 'not_connected', detail: 'secret' })], + [ + 'an oversized response', + jsonResponse( + { status: 'temporarily_unavailable' }, + { headers: { 'Content-Length': '65536' } } + ), + ], + ['a non-JSON response', new Response('unavailable', { status: 200 })], + ])('maps %s to temporary unavailability', async (_name, response) => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce(response); + + await expect( + fetchGitLabCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: '22222222-2222-4222-8222-222222222222' } + ) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + }); +}); + +describe('GitLabCredentialBrokerResultSchema', () => { + it.each([ + 'invalid_request', + 'not_connected', + 'reconnect_required', + 'temporarily_unavailable', + ] as const)('accepts the strict %s result', status => { + expect(GitLabCredentialBrokerResultSchema.parse({ status })).toEqual({ status }); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-broker-client.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-broker-client.ts new file mode 100644 index 0000000000..f47294ea8d --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-broker-client.ts @@ -0,0 +1,139 @@ +import 'server-only'; + +import { z } from 'zod'; +import { GITLAB_CREDENTIAL_BROKER_AUDIENCE } from '@kilocode/worker-utils/internal-service-token-audiences'; +import { GIT_TOKEN_SERVICE_API_URL } from '@/lib/config.server'; +import { generateInternalServiceToken, TOKEN_EXPIRY } from '@/lib/tokens'; + +const GITLAB_CREDENTIAL_RESPONSE_MAX_BYTES = 16_384; +const GITLAB_CREDENTIAL_REQUEST_TIMEOUT_MS = 30_000; + +export const GitLabCredentialSelectorSchema = z.discriminatedUnion('credential', [ + z + .object({ + credential: z.literal('integration'), + integrationId: z.uuid(), + }) + .strict(), + z + .object({ + credential: z.literal('project-exact'), + integrationId: z.uuid(), + projectId: z.string().regex(/^[1-9][0-9]*$/), + }) + .strict(), +]); + +export const GitLabCredentialBrokerResultSchema = z.discriminatedUnion('status', [ + z + .object({ + status: z.literal('available'), + token: z.string().min(1).max(10_000), + instanceUrl: z.url().max(2048), + glabIsOAuth2: z.boolean(), + }) + .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('temporarily_unavailable') }).strict(), +]); + +export type GitLabCredentialActor = { + userId: string; + organizationId?: string; +}; +export type GitLabCredentialSelector = z.infer; +export type GitLabCredentialBrokerResult = z.infer; + +async function readBoundedJson(response: Response): Promise { + if (!response.body) throw new Error('invalid_response'); + const contentType = response.headers.get('Content-Type')?.split(';', 1)[0].trim().toLowerCase(); + if (contentType !== 'application/json') throw new Error('invalid_response'); + const contentLength = response.headers.get('Content-Length'); + if ( + contentLength && + (!/^[0-9]+$/.test(contentLength) || + Number(contentLength) > GITLAB_CREDENTIAL_RESPONSE_MAX_BYTES) + ) { + throw new Error('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; + if (!(chunk.value instanceof Uint8Array)) throw new Error('invalid_response'); + totalBytes += chunk.value.byteLength; + if (totalBytes > GITLAB_CREDENTIAL_RESPONSE_MAX_BYTES) { + try { + await reader.cancel(); + } catch { + // The response remains rejected when cancellation itself fails. + } + throw new Error('invalid_response'); + } + chunks.push(chunk.value); + } + } finally { + reader.releaseLock(); + } + + const body = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body)); +} + +export async function fetchGitLabCredential( + actor: GitLabCredentialActor, + selector: GitLabCredentialSelector +): Promise { + const parsedSelector = GitLabCredentialSelectorSchema.safeParse(selector); + if (!parsedSelector.success) return { status: 'invalid_request' }; + if (!GIT_TOKEN_SERVICE_API_URL) return { status: 'temporarily_unavailable' }; + + let serviceToken: string; + try { + serviceToken = generateInternalServiceToken(actor.userId, { + expiresIn: TOKEN_EXPIRY.fiveMinutes, + audience: GITLAB_CREDENTIAL_BROKER_AUDIENCE, + organizationId: actor.organizationId, + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + + let response: Response; + try { + response = await fetch(`${GIT_TOKEN_SERVICE_API_URL}/internal/gitlab/credentials`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${serviceToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(parsedSelector.data), + redirect: 'error', + signal: AbortSignal.timeout(GITLAB_CREDENTIAL_REQUEST_TIMEOUT_MS), + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (!response.ok || response.redirected) return { status: 'temporarily_unavailable' }; + + try { + const parsedResult = GitLabCredentialBrokerResultSchema.safeParse( + await readBoundedJson(response) + ); + return parsedResult.success ? parsedResult.data : { status: 'temporarily_unavailable' }; + } catch { + return { status: 'temporarily_unavailable' }; + } +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-encryption.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-encryption.test.ts new file mode 100644 index 0000000000..d29a0bb0b1 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-encryption.test.ts @@ -0,0 +1,205 @@ +import { beforeEach, describe, expect, it } from '@jest/globals'; +import { createHash, createPublicKey, generateKeyPairSync } from 'node:crypto'; +import { decryptKeyedEnvelope } from '@kilocode/encryption'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, +} from '@kilocode/worker-utils/gitlab-credential'; +import { + encryptGitLabOAuthCredentials, + encryptGitLabPersonalAccessToken, + encryptGitLabProjectAccessToken, + getGitLabCredentialEncryptionPublicKeyInfo, +} from './credential-encryption'; + +const testKeyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}); +const nonRsaPublicKey = generateKeyPairSync('ec', { + namedCurve: 'P-256', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}).publicKey; +const TEST_KEY_ID = 'platform-credential-key-v1'; +const mockCredentialEncryptionConfig: { + keyId: string | undefined; + publicKey: string | undefined; +} = { + keyId: TEST_KEY_ID, + publicKey: Buffer.from(testKeyPair.publicKey).toString('base64'), +}; + +jest.mock('@/lib/config.server', () => ({ + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID() { + return mockCredentialEncryptionConfig.keyId; + }, + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY() { + return mockCredentialEncryptionConfig.publicKey; + }, +})); + +const oauthContext = { + credentialId: 'credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com/root', + owner: { type: 'org', id: 'organization-1' } as const, + authorizedByUserId: 'user-1', + credentialVersion: 1, +}; + +describe('GitLab credential encryption', () => { + beforeEach(() => { + mockCredentialEncryptionConfig.keyId = TEST_KEY_ID; + mockCredentialEncryptionConfig.publicKey = Buffer.from(testKeyPair.publicKey).toString( + 'base64' + ); + }); + + it('encrypts every OAuth secret kind with its bound AAD', () => { + const plaintext = { + accessToken: 'gitlab-access-token', + refreshToken: 'gitlab-refresh-token', + oauthClientSecret: 'self-hosted-client-secret', + }; + + const encrypted = encryptGitLabOAuthCredentials({ ...oauthContext, ...plaintext }); + const privateKeys = { + active: { + keyId: TEST_KEY_ID, + privateKeyPem: testKeyPair.privateKey, + }, + }; + const decrypt = (ciphertext: string, kind: 'access' | 'refresh' | 'oauth-client-secret') => + decryptKeyedEnvelope( + ciphertext, + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + privateKeys, + buildGitLabOAuthCredentialAad({ ...oauthContext, kind }) + ); + + expect(decrypt(encrypted.accessTokenEncrypted, 'access')).toBe(plaintext.accessToken); + expect(decrypt(encrypted.refreshTokenEncrypted, 'refresh')).toBe(plaintext.refreshToken); + if (!encrypted.oauthClientSecretEncrypted) { + throw new Error('Expected encrypted custom client secret'); + } + expect(decrypt(encrypted.oauthClientSecretEncrypted, 'oauth-client-secret')).toBe( + plaintext.oauthClientSecret + ); + expect(JSON.stringify(encrypted)).not.toContain(plaintext.accessToken); + expect(JSON.stringify(encrypted)).not.toContain(plaintext.refreshToken); + expect(JSON.stringify(encrypted)).not.toContain(plaintext.oauthClientSecret); + }); + + it('reports only the configured key ID and canonical public-key fingerprint', () => { + const publicKeyDer = createPublicKey(testKeyPair.publicKey).export({ + type: 'spki', + format: 'der', + }); + + expect(getGitLabCredentialEncryptionPublicKeyInfo()).toEqual({ + keyId: TEST_KEY_ID, + publicKeySha256: createHash('sha256').update(publicKeyDer).digest('hex'), + }); + }); + + it('leaves the optional custom client-secret ciphertext null when none is supplied', () => { + const encrypted = encryptGitLabOAuthCredentials({ + ...oauthContext, + accessToken: 'gitlab-access-token', + refreshToken: 'gitlab-refresh-token', + oauthClientSecret: null, + }); + + expect(encrypted.oauthClientSecretEncrypted).toBeNull(); + expect(encrypted.accessTokenEncrypted).toEqual(expect.any(String)); + expect(encrypted.refreshTokenEncrypted).toEqual(expect.any(String)); + }); + + it('encrypts a personal access token with supplier-bound AAD', () => { + const input = { + credentialId: 'credential-2', + integrationId: 'integration-2', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-2' } as const, + authorizedByUserId: 'user-2', + credentialVersion: 3, + token: 'glpat-secret-value-123', + }; + + const encrypted = encryptGitLabPersonalAccessToken(input); + + expect( + decryptKeyedEnvelope( + encrypted, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + { + active: { + keyId: TEST_KEY_ID, + privateKeyPem: testKeyPair.privateKey, + }, + }, + buildGitLabPersonalAccessTokenAad(input) + ) + ).toBe(input.token); + expect(encrypted).not.toContain(input.token); + }); + + it('encrypts a project access token with resource-bound AAD', () => { + const input = { + credentialId: 'credential-3', + integrationId: 'integration-3', + providerBaseUrl: 'https://gitlab.example.com/root', + owner: { type: 'org', id: 'organization-3' } as const, + providerResourceId: '42', + credentialVersion: 2, + token: 'glpat-project-secret-456', + }; + + const encrypted = encryptGitLabProjectAccessToken(input); + + expect( + decryptKeyedEnvelope( + encrypted, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + { + active: { + keyId: TEST_KEY_ID, + privateKeyPem: testKeyPair.privateKey, + }, + }, + buildGitLabProjectAccessTokenAad(input) + ) + ).toBe(input.token); + expect(encrypted).not.toContain(input.token); + }); + + it.each([ + ['missing key ID', undefined, Buffer.from(testKeyPair.publicKey).toString('base64')], + ['whitespace key ID', ' key-v1 ', Buffer.from(testKeyPair.publicKey).toString('base64')], + ['missing public key', 'key-v1', undefined], + ['malformed public key', 'key-v1', Buffer.from('not a public key').toString('base64')], + ['private key material', 'key-v1', Buffer.from(testKeyPair.privateKey).toString('base64')], + ['non-RSA public key', 'key-v1', Buffer.from(nonRsaPublicKey).toString('base64')], + ])('rejects %s before producing ciphertext', (_label, keyId, publicKey) => { + mockCredentialEncryptionConfig.keyId = keyId; + mockCredentialEncryptionConfig.publicKey = publicKey; + + expect(() => + encryptGitLabPersonalAccessToken({ + credentialId: 'credential-invalid', + integrationId: 'integration-invalid', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-invalid' }, + authorizedByUserId: 'user-invalid', + credentialVersion: 1, + token: 'must-not-be-encrypted', + }) + ).toThrow('GitLab credential encryption is not configured'); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-encryption.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-encryption.ts new file mode 100644 index 0000000000..24e6b07f2d --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-encryption.ts @@ -0,0 +1,131 @@ +import 'server-only'; + +import { createHash, createPublicKey } from 'node:crypto'; +import { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID, + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY, +} from '@/lib/config.server'; +import { encryptKeyedEnvelope } from '@kilocode/encryption'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, + type GitLabOAuthCredentialAadInput, + type GitLabPersonalAccessTokenAadInput, + type GitLabProjectAccessTokenAadInput, +} from '@kilocode/worker-utils/gitlab-credential'; + +type CredentialEncryptionKey = { + keyId: string; + publicKeyPem: Buffer; + publicKeySha256: string; +}; + +export class GitLabCredentialEncryptionError extends Error { + constructor() { + super('GitLab credential encryption is not configured'); + this.name = 'GitLabCredentialEncryptionError'; + } +} + +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 GitLabCredentialEncryptionError(); + } + + const publicKeyPem = Buffer.from(encodedPublicKey, 'base64'); + let publicKeySha256: string; + 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'); + } + publicKeySha256 = createHash('sha256') + .update(publicKey.export({ type: 'spki', format: 'der' })) + .digest('hex'); + } catch { + throw new GitLabCredentialEncryptionError(); + } + + return { keyId, publicKeyPem, publicKeySha256 }; +} + +export function getGitLabCredentialEncryptionPublicKeyInfo(): { + keyId: string; + publicKeySha256: string; +} { + const { keyId, publicKeySha256 } = requireCredentialEncryptionKey(); + return { keyId, publicKeySha256 }; +} + +export type EncryptGitLabOAuthCredentialsInput = Omit & { + accessToken: string; + refreshToken: string; + oauthClientSecret: string | null; +}; + +export type EncryptedGitLabOAuthCredentials = { + accessTokenEncrypted: string; + refreshTokenEncrypted: string; + oauthClientSecretEncrypted: string | null; +}; + +export function encryptGitLabOAuthCredentials( + input: EncryptGitLabOAuthCredentialsInput +): EncryptedGitLabOAuthCredentials { + const encryptionKey = requireCredentialEncryptionKey(); + const encrypt = (value: string, kind: GitLabOAuthCredentialAadInput['kind']) => + encryptKeyedEnvelope( + value, + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + encryptionKey, + buildGitLabOAuthCredentialAad({ ...input, kind }) + ); + + return { + accessTokenEncrypted: encrypt(input.accessToken, 'access'), + refreshTokenEncrypted: encrypt(input.refreshToken, 'refresh'), + oauthClientSecretEncrypted: + input.oauthClientSecret === null + ? null + : encrypt(input.oauthClientSecret, 'oauth-client-secret'), + }; +} + +export type EncryptGitLabPersonalAccessTokenInput = GitLabPersonalAccessTokenAadInput & { + token: string; +}; + +export function encryptGitLabPersonalAccessToken( + input: EncryptGitLabPersonalAccessTokenInput +): string { + return encryptKeyedEnvelope( + input.token, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + requireCredentialEncryptionKey(), + buildGitLabPersonalAccessTokenAad(input) + ); +} + +export type EncryptGitLabProjectAccessTokenInput = GitLabProjectAccessTokenAadInput & { + token: string; +}; + +export function encryptGitLabProjectAccessToken( + input: EncryptGitLabProjectAccessTokenInput +): string { + return encryptKeyedEnvelope( + input.token, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + requireCredentialEncryptionKey(), + buildGitLabProjectAccessTokenAad(input) + ); +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-audit.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-audit.ts new file mode 100644 index 0000000000..25e06f15e1 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-audit.ts @@ -0,0 +1,138 @@ +import { + type PlatformAccessTokenCredential, + type PlatformIntegration, + type PlatformOAuthCredential, +} from '@kilocode/db/schema'; +import { + GitLabOAuthCredentialRowSchema, + GitLabPersonalAccessTokenCredentialRowSchema, + GitLabPersonalAccessTokenMetadataSchema, + GitLabProjectAccessTokenCredentialRowSchema, + GitLabProjectAccessTokenMetadataSchema, +} from '@kilocode/worker-utils/gitlab-credential'; +import { getGitLabIntegrationOwner } from './credential-migration-legacy'; + +export type GitLabCredentialAuditCounts = { + legacyTokenBearingIntegrations: number; + oauthMissingCredentials: number; + patMissingCredentials: number; + projectMissingCredentials: number; + credentialProfileMismatches: number; + providerMetadataMismatches: number; + crossTablePrimaryCredentialDuplicates: number; + malformedMetadata: number; + unmappableLegacyEntries: number; + integrationTypeDisagreements: number; + legacySecretFields: number; +}; + +export function emptyGitLabCredentialAuditCounts(): GitLabCredentialAuditCounts { + return { + legacyTokenBearingIntegrations: 0, + oauthMissingCredentials: 0, + patMissingCredentials: 0, + projectMissingCredentials: 0, + credentialProfileMismatches: 0, + providerMetadataMismatches: 0, + crossTablePrimaryCredentialDuplicates: 0, + malformedMetadata: 0, + unmappableLegacyEntries: 0, + integrationTypeDisagreements: 0, + legacySecretFields: 0, + }; +} + +export function hasBlockingGitLabCredentialAuditIssues( + counts: GitLabCredentialAuditCounts +): boolean { + return ( + counts.oauthMissingCredentials > 0 || + counts.patMissingCredentials > 0 || + counts.projectMissingCredentials > 0 || + counts.credentialProfileMismatches > 0 || + counts.providerMetadataMismatches > 0 || + counts.crossTablePrimaryCredentialDuplicates > 0 || + counts.malformedMetadata > 0 || + counts.unmappableLegacyEntries > 0 || + counts.integrationTypeDisagreements > 0 + ); +} + +export function auditGitLabCredentialProfiles( + integration: PlatformIntegration, + providerBaseUrl: string, + oauthCredential: PlatformOAuthCredential | undefined, + accessCredentials: PlatformAccessTokenCredential[] +): { profileMismatches: number; providerMetadataMismatches: number } { + const owner = getGitLabIntegrationOwner(integration); + const authorizerMatchesOwner = (authorizedByUserId: string | null) => + owner.type === 'user' + ? authorizedByUserId === owner.id + : authorizedByUserId === null || authorizedByUserId.length > 0; + let profileMismatches = 0; + let providerMetadataMismatches = 0; + + if (oauthCredential) { + const matchesParent = + integration.integration_type === 'oauth' && + authorizerMatchesOwner(oauthCredential.authorized_by_user_id) && + oauthCredential.provider_subject_id === integration.platform_account_id && + oauthCredential.provider_subject_login === integration.platform_account_login && + oauthCredential.provider_base_url === providerBaseUrl; + if (!matchesParent || !GitLabOAuthCredentialRowSchema.safeParse(oauthCredential).success) { + profileMismatches += 1; + } + } + + for (const credential of accessCredentials) { + if (credential.provider_credential_type === 'personal_access_token') { + const providerMetadataResult = GitLabPersonalAccessTokenMetadataSchema.safeParse( + credential.provider_metadata + ); + if (!providerMetadataResult.success) providerMetadataMismatches += 1; + const schemaInput = providerMetadataResult.success + ? credential + : { ...credential, provider_metadata: {} }; + const matchesParent = + integration.integration_type === 'pat' && + authorizerMatchesOwner(credential.authorized_by_user_id) && + credential.provider_resource_id === null && + credential.provider_base_url === providerBaseUrl; + if ( + !matchesParent || + !GitLabPersonalAccessTokenCredentialRowSchema.safeParse(schemaInput).success + ) { + profileMismatches += 1; + } + continue; + } + + if (credential.provider_credential_type === 'project_access_token') { + const providerMetadataResult = GitLabProjectAccessTokenMetadataSchema.safeParse( + credential.provider_metadata + ); + if (!providerMetadataResult.success) providerMetadataMismatches += 1; + const schemaInput = providerMetadataResult.success + ? credential + : { + ...credential, + provider_metadata: { providerCredentialId: '1', expiresOn: '2030-01-01' }, + }; + const matchesParent = + (integration.integration_type === 'oauth' || integration.integration_type === 'pat') && + credential.authorized_by_user_id === null && + credential.provider_base_url === providerBaseUrl; + if ( + !matchesParent || + !GitLabProjectAccessTokenCredentialRowSchema.safeParse(schemaInput).success + ) { + profileMismatches += 1; + } + continue; + } + + profileMismatches += 1; + } + + return { profileMismatches, providerMetadataMismatches }; +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-backfill.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-backfill.ts new file mode 100644 index 0000000000..3baabefd83 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-backfill.ts @@ -0,0 +1,188 @@ +import { randomUUID } from 'node:crypto'; +import type { DrizzleTransaction } from '@/lib/drizzle'; +import { + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, + type PlatformIntegration, +} from '@kilocode/db/schema'; +import { eq } from 'drizzle-orm'; +import { + GitLabPersonalAccessTokenMetadataSchema, + GitLabProjectAccessTokenMetadataSchema, +} from '@kilocode/worker-utils/gitlab-credential'; +import { + encryptGitLabOAuthCredentials, + encryptGitLabPersonalAccessToken, + encryptGitLabProjectAccessToken, +} from './credential-encryption'; +import { + getGitLabIntegrationOwner, + type GitLabLegacyMetadata, +} from './credential-migration-legacy'; +import { normalizeGitLabInstanceUrl } from './instance-url'; + +type ExistingCredentialRows = { + oauth: typeof platform_oauth_credentials.$inferSelect | undefined; + primaryAccess: typeof platform_access_token_credentials.$inferSelect | undefined; + access: (typeof platform_access_token_credentials.$inferSelect)[]; +}; + +export type GitLabBackfillResult = { + mutated: boolean; + unmappableProjects: number; +}; + +export async function backfillMissingGitLabCredentials( + tx: DrizzleTransaction, + integration: PlatformIntegration, + metadata: GitLabLegacyMetadata, + existing: ExistingCredentialRows +): Promise { + const owner = getGitLabIntegrationOwner(integration); + const providerBaseUrl = normalizeGitLabInstanceUrl(metadata.gitlab_instance_url); + let mutated = false; + let primaryInserted = false; + + if ( + metadata.auth_type === 'oauth' && + metadata.access_token && + metadata.refresh_token && + integration.platform_account_id && + integration.platform_account_login && + (metadata.client_id === undefined) === (metadata.client_secret === undefined) && + !existing.oauth && + !existing.primaryAccess + ) { + const credentialId = randomUUID(); + const credentialVersion = 1; + const authorizedByUserId = owner.type === 'user' ? owner.id : null; + const encrypted = encryptGitLabOAuthCredentials({ + credentialId, + integrationId: integration.id, + providerBaseUrl, + owner, + authorizedByUserId, + credentialVersion, + accessToken: metadata.access_token, + refreshToken: metadata.refresh_token, + oauthClientSecret: metadata.client_secret ?? null, + }); + const inserted = await tx + .insert(platform_oauth_credentials) + .values({ + id: credentialId, + platform_integration_id: integration.id, + authorized_by_user_id: authorizedByUserId, + provider_subject_id: integration.platform_account_id, + provider_subject_login: integration.platform_account_login, + provider_base_url: providerBaseUrl, + access_token_encrypted: encrypted.accessTokenEncrypted, + access_token_expires_at: metadata.token_expires_at ?? null, + refresh_token_encrypted: encrypted.refreshTokenEncrypted, + oauth_client_secret_encrypted: encrypted.oauthClientSecretEncrypted, + credential_version: credentialVersion, + }) + .onConflictDoNothing() + .returning({ id: platform_oauth_credentials.id }); + primaryInserted = inserted.length === 1; + mutated ||= primaryInserted; + } else if ( + metadata.auth_type === 'pat' && + metadata.access_token && + !existing.primaryAccess && + !existing.oauth + ) { + const credentialId = randomUUID(); + const credentialVersion = 1; + const authorizedByUserId = owner.type === 'user' ? owner.id : null; + const tokenEncrypted = encryptGitLabPersonalAccessToken({ + token: metadata.access_token, + credentialId, + integrationId: integration.id, + providerBaseUrl, + owner, + authorizedByUserId, + credentialVersion, + }); + const inserted = await tx + .insert(platform_access_token_credentials) + .values({ + id: credentialId, + platform_integration_id: integration.id, + token_encrypted: tokenEncrypted, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: providerBaseUrl, + authorized_by_user_id: authorizedByUserId, + provider_metadata: GitLabPersonalAccessTokenMetadataSchema.parse({}), + provider_scopes: integration.scopes ?? null, + credential_version: credentialVersion, + }) + .onConflictDoNothing() + .returning({ id: platform_access_token_credentials.id }); + primaryInserted = inserted.length === 1; + mutated ||= primaryInserted; + } + + if (primaryInserted && metadata.auth_type !== integration.integration_type) { + await tx + .update(platform_integrations) + .set({ integration_type: metadata.auth_type, updated_at: new Date().toISOString() }) + .where(eq(platform_integrations.id, integration.id)); + } + + const integrationType = primaryInserted + ? metadata.auth_type + : integration.integration_type === 'oauth' || integration.integration_type === 'pat' + ? integration.integration_type + : null; + let unmappableProjects = 0; + const existingProjectIds = new Set( + existing.access.flatMap(row => + row.provider_credential_type === 'project_access_token' && row.provider_resource_id + ? [row.provider_resource_id] + : [] + ) + ); + for (const [projectId, projectToken] of Object.entries(metadata.project_tokens ?? {})) { + if (existingProjectIds.has(projectId)) continue; + if (!integrationType || !/^[1-9][0-9]*$/.test(projectId)) { + unmappableProjects += 1; + continue; + } + const credentialId = randomUUID(); + const credentialVersion = 1; + const tokenEncrypted = encryptGitLabProjectAccessToken({ + token: projectToken.token, + credentialId, + integrationId: integration.id, + providerBaseUrl, + owner, + providerResourceId: projectId, + credentialVersion, + }); + const inserted = await tx + .insert(platform_access_token_credentials) + .values({ + id: credentialId, + platform_integration_id: integration.id, + token_encrypted: tokenEncrypted, + provider_credential_type: 'project_access_token', + provider_resource_id: projectId, + provider_base_url: providerBaseUrl, + authorized_by_user_id: null, + provider_metadata: GitLabProjectAccessTokenMetadataSchema.parse({ + providerCredentialId: String(projectToken.token_id), + expiresOn: projectToken.expires_at, + }), + provider_scopes: null, + credential_version: credentialVersion, + }) + .onConflictDoNothing() + .returning({ id: platform_access_token_credentials.id }); + mutated ||= inserted.length === 1; + } + + return { mutated, unmappableProjects }; +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-legacy.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-legacy.ts new file mode 100644 index 0000000000..ad50b66f41 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration-legacy.ts @@ -0,0 +1,61 @@ +import type { PlatformIntegration } from '@kilocode/db/schema'; +import { z } from 'zod'; + +const LegacyProjectTokenSchema = z + .object({ + token_id: z.number().int().positive(), + token: z.string().min(1), + expires_at: z.iso.date(), + created_at: z.string().min(1), + name: z.string().min(1), + }) + .strict(); + +export const GitLabLegacyMetadataSchema = z + .object({ + access_token: z.string().min(1).optional(), + refresh_token: z.string().min(1).optional(), + token_expires_at: z.iso.datetime({ offset: true }).optional(), + client_id: z.string().min(1).optional(), + client_secret: z.string().min(1).optional(), + gitlab_instance_url: z.string().min(1).optional(), + auth_type: z.enum(['oauth', 'pat']).optional(), + project_tokens: z.record(z.string(), LegacyProjectTokenSchema).optional(), + }) + .passthrough(); + +export type GitLabLegacyMetadata = z.infer; + +export function getGitLabIntegrationOwner(integration: PlatformIntegration) { + if (integration.owned_by_user_id) { + return { type: 'user', id: integration.owned_by_user_id } as const; + } + if (integration.owned_by_organization_id) { + return { type: 'org', id: integration.owned_by_organization_id } as const; + } + throw new Error('GitLab integration has no owner'); +} + +export function countLegacySecretFields(metadata: Record): number { + return [ + 'access_token', + 'refresh_token', + 'token_expires_at', + 'client_secret', + 'project_tokens', + ].filter(key => key in metadata).length; +} + +export function hasTokenBearingLegacyMetadata(metadata: Record): boolean { + if ('access_token' in metadata || 'refresh_token' in metadata || 'client_secret' in metadata) { + return true; + } + if (!('project_tokens' in metadata)) return false; + const projectTokens = metadata.project_tokens; + return ( + typeof projectTokens !== 'object' || + projectTokens === null || + Array.isArray(projectTokens) || + Object.keys(projectTokens).length > 0 + ); +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-migration.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration.test.ts new file mode 100644 index 0000000000..93ba92250c --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration.test.ts @@ -0,0 +1,720 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ + +import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; +import { generateKeyPairSync } from 'node:crypto'; +import { decryptKeyedEnvelope } from '@kilocode/encryption'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, +} from '@kilocode/worker-utils/gitlab-credential'; +import { db } from '@/lib/drizzle'; +import { + kilocode_users, + organizations, + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { eq } from 'drizzle-orm'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; +import { runGitLabCredentialMigration } from './credential-migration'; + +const testKeyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}); +const TEST_KEY_ID = 'platform-credential-key-v1'; +const mockCredentialEncryptionConfig: { + keyId: string | undefined; + publicKey: string | undefined; +} = { + keyId: TEST_KEY_ID, + publicKey: Buffer.from(testKeyPair.publicKey).toString('base64'), +}; + +jest.mock('@/lib/config.server', () => ({ + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID() { + return mockCredentialEncryptionConfig.keyId; + }, + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY() { + return mockCredentialEncryptionConfig.publicKey; + }, +})); + +describe('GitLab credential migration', () => { + beforeEach(() => { + mockCredentialEncryptionConfig.keyId = TEST_KEY_ID; + mockCredentialEncryptionConfig.publicKey = Buffer.from(testKeyPair.publicKey).toString( + 'base64' + ); + }); + + afterEach(async () => { + 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); + }); + + it('audits legacy OAuth, PAT, and project tokens without changing integrations', async () => { + const user = await insertTestUser(); + const fixedUpdatedAt = '2026-07-01T00:00:00.000Z'; + const [oauthIntegration, patIntegration] = await db + .insert(platform_integrations) + .values([ + { + id: '00000000-0000-4000-8000-000000000001', + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: '101', + platform_account_login: 'oauth-user', + integration_status: 'active', + updated_at: fixedUpdatedAt, + metadata: { + access_token: 'oauth-access-secret', + refresh_token: 'oauth-refresh-secret', + token_expires_at: '2030-01-01T00:00:00.000Z', + client_id: 'oauth-client-id', + client_secret: 'oauth-client-secret', + auth_type: 'oauth', + webhook_secret: 'keep-webhook-secret', + project_tokens: { + '42': { + token_id: 99, + token: 'project-secret', + expires_at: '2030-06-01', + created_at: '2026-07-01T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }, + }, + }, + }, + { + id: '00000000-0000-4000-8000-000000000002', + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: '202', + platform_account_login: 'pat-user', + integration_status: 'suspended', + updated_at: fixedUpdatedAt, + metadata: { access_token: 'pat-secret', auth_type: 'pat' }, + }, + ]) + .returning(); + if (!oauthIntegration || !patIntegration) throw new Error('Expected integrations'); + + const result = await runGitLabCredentialMigration(); + + expect(result).toEqual({ + mode: 'audit', + applied: false, + scannedIntegrations: 2, + mutatedIntegrations: 0, + counts: { + legacyTokenBearingIntegrations: 2, + oauthMissingCredentials: 1, + patMissingCredentials: 1, + projectMissingCredentials: 1, + credentialProfileMismatches: 0, + providerMetadataMismatches: 0, + crossTablePrimaryCredentialDuplicates: 0, + malformedMetadata: 0, + unmappableLegacyEntries: 0, + integrationTypeDisagreements: 0, + legacySecretFields: 6, + }, + integrationIds: [oauthIntegration.id, patIntegration.id], + }); + const rowsAfter = await db + .select({ id: platform_integrations.id, updatedAt: platform_integrations.updated_at }) + .from(platform_integrations) + .where(eq(platform_integrations.platform, 'gitlab')); + expect(rowsAfter).toEqual([ + { id: oauthIntegration.id, updatedAt: expect.stringContaining('2026-07-01') }, + { id: patIntegration.id, updatedAt: expect.stringContaining('2026-07-01') }, + ]); + }); + + it('inserts a missing OAuth credential from freshly locked legacy metadata', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com/root'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: '101', + platform_account_login: 'oauth-user', + integration_status: 'active', + metadata: { + access_token: 'oauth-access-secret', + refresh_token: 'oauth-refresh-secret', + token_expires_at: '2030-01-01T00:00:00.000Z', + client_id: 'oauth-client-id', + client_secret: 'oauth-client-secret', + gitlab_instance_url: providerBaseUrl, + auth_type: 'oauth', + webhook_secret: 'keep-webhook-secret', + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + + const result = await runGitLabCredentialMigration({ + mode: 'backfill', + apply: true, + batchSize: 1, + }); + + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + const [credential] = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)); + if (!credential) throw new Error('Expected OAuth credential'); + + expect(result.applied).toBe(true); + expect(result.mutatedIntegrations).toBe(1); + expect(updatedIntegration?.integration_type).toBe('oauth'); + expect(credential).toEqual( + expect.objectContaining({ + authorized_by_user_id: user.id, + provider_subject_id: '101', + provider_subject_login: 'oauth-user', + provider_base_url: providerBaseUrl, + credential_version: 1, + }) + ); + const privateKeys = { + active: { keyId: TEST_KEY_ID, privateKeyPem: testKeyPair.privateKey }, + }; + const decrypt = (ciphertext: string, kind: 'access' | 'refresh' | 'oauth-client-secret') => + decryptKeyedEnvelope( + ciphertext, + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + privateKeys, + buildGitLabOAuthCredentialAad({ + credentialId: credential.id, + integrationId: integration.id, + providerBaseUrl, + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + credentialVersion: credential.credential_version, + kind, + }) + ); + expect(decrypt(credential.access_token_encrypted, 'access')).toBe('oauth-access-secret'); + if (!credential.refresh_token_encrypted || !credential.oauth_client_secret_encrypted) { + throw new Error('Expected complete OAuth credential'); + } + expect(decrypt(credential.refresh_token_encrypted, 'refresh')).toBe('oauth-refresh-secret'); + expect(decrypt(credential.oauth_client_secret_encrypted, 'oauth-client-secret')).toBe( + 'oauth-client-secret' + ); + }); + + it('backfills a personal access token with known legacy ownership and no fabricated evidence', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: '202', + platform_account_login: 'pat-user', + scopes: ['api'], + integration_status: 'suspended', + metadata: { + access_token: 'pat-secret', + gitlab_instance_url: providerBaseUrl, + auth_type: 'pat', + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + + await runGitLabCredentialMigration({ mode: 'backfill', apply: true }); + + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + if (!credential) throw new Error('Expected PAT credential'); + expect(credential).toEqual( + expect.objectContaining({ + expires_at: null, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: providerBaseUrl, + authorized_by_user_id: user.id, + provider_metadata: {}, + provider_scopes: ['api'], + provider_verified_at: null, + last_validated_at: null, + credential_version: 1, + }) + ); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId: TEST_KEY_ID, privateKeyPem: testKeyPair.privateKey } }, + buildGitLabPersonalAccessTokenAad({ + credentialId: credential.id, + integrationId: integration.id, + providerBaseUrl, + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + credentialVersion: 1, + }) + ) + ).toBe('pat-secret'); + }); + + it('backfills project tokens for a suspended integration without a primary token', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com/root'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: '303', + platform_account_login: 'disconnected-user', + integration_status: 'suspended', + metadata: { + gitlab_instance_url: providerBaseUrl, + auth_type: 'oauth', + project_tokens: { + '42': { + token_id: 99, + token: 'project-secret', + expires_at: '2030-06-01', + created_at: '2026-07-01T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }, + }, + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + + await runGitLabCredentialMigration({ mode: 'backfill', apply: true }); + + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + if (!credential) throw new Error('Expected project credential'); + expect(credential).toEqual( + expect.objectContaining({ + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + provider_base_url: providerBaseUrl, + authorized_by_user_id: null, + provider_metadata: { providerCredentialId: '99', expiresOn: '2030-06-01' }, + provider_scopes: null, + provider_verified_at: null, + last_validated_at: null, + credential_version: 1, + }) + ); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId: TEST_KEY_ID, privateKeyPem: testKeyPair.privateKey } }, + buildGitLabProjectAccessTokenAad({ + credentialId: credential.id, + integrationId: integration.id, + providerBaseUrl, + owner: { type: 'user', id: user.id }, + providerResourceId: '42', + credentialVersion: 1, + }) + ) + ).toBe('project-secret'); + }); + + it('scrubs only migrated legacy secret fields and preserves GitLab configuration', async () => { + const user = await insertTestUser(); + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: '404', + platform_account_login: 'oauth-user', + integration_status: 'active', + metadata: { + access_token: 'oauth-access-secret', + refresh_token: 'oauth-refresh-secret', + token_expires_at: '2030-01-01T00:00:00.000Z', + client_id: 'custom-client-id', + client_secret: 'oauth-client-secret', + gitlab_instance_url: 'https://gitlab.example.com', + auth_type: 'oauth', + webhook_secret: 'keep-webhook-secret', + configured_webhooks: { '42': { hook_id: 7 } }, + notes: 'keep-non-secret-metadata', + project_tokens: { + '42': { + token_id: 99, + token: 'project-secret', + expires_at: '2030-06-01', + created_at: '2026-07-01T00:00:00.000Z', + name: 'Kilo Code Review Bot', + }, + }, + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + await runGitLabCredentialMigration({ mode: 'backfill', apply: true }); + + const result = await runGitLabCredentialMigration({ + mode: 'scrub', + apply: true, + privateAuditPassed: true, + }); + + const [scrubbed] = await db + .select({ metadata: platform_integrations.metadata }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(result.applied).toBe(true); + expect(result.mutatedIntegrations).toBe(1); + expect(scrubbed?.metadata).toEqual({ + client_id: 'custom-client-id', + gitlab_instance_url: 'https://gitlab.example.com', + auth_type: 'oauth', + webhook_secret: 'keep-webhook-secret', + configured_webhooks: { '42': { hook_id: 7 } }, + notes: 'keep-non-secret-metadata', + }); + const auditAfter = await runGitLabCredentialMigration(); + expect(auditAfter.counts.legacySecretFields).toBe(0); + expect(auditAfter.counts.legacyTokenBearingIntegrations).toBe(0); + expect(auditAfter.counts.unmappableLegacyEntries).toBe(0); + }); + + it('audits provider metadata separately from parent credential-profile mismatches', async () => { + const user = await insertTestUser(); + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: '505', + platform_account_login: 'pat-user', + integration_status: 'active', + metadata: { + access_token: 'pat-secret', + gitlab_instance_url: 'https://gitlab.example.com', + auth_type: 'pat', + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: integration.id, + token_encrypted: 'persisted-envelope', + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: 'https://other-gitlab.example.com', + authorized_by_user_id: null, + provider_metadata: { expiresOn: 'not-a-date' }, + }); + + const result = await runGitLabCredentialMigration(); + + expect(result.counts.patMissingCredentials).toBe(0); + expect(result.counts.credentialProfileMismatches).toBe(1); + expect(result.counts.providerMetadataMismatches).toBe(1); + expect(result.integrationIds).toEqual([integration.id]); + }); + + it('accepts non-null supplying users on organization OAuth and PAT credentials', async () => { + const user = await insertTestUser(); + const oauthOrganization = await createTestOrganization('OAuth Organization', user.id, 0); + const patOrganization = await createTestOrganization('PAT Organization', user.id, 0); + const providerBaseUrl = 'https://gitlab.example.com'; + const [oauthIntegration, patIntegration] = await db + .insert(platform_integrations) + .values([ + { + owned_by_organization_id: oauthOrganization.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: '551', + platform_account_login: 'oauth-user', + metadata: { + access_token: 'oauth-access-secret', + refresh_token: 'oauth-refresh-secret', + gitlab_instance_url: providerBaseUrl, + auth_type: 'oauth', + }, + }, + { + owned_by_organization_id: patOrganization.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: '552', + platform_account_login: 'pat-user', + metadata: { + access_token: 'pat-secret', + gitlab_instance_url: providerBaseUrl, + auth_type: 'pat', + }, + }, + ]) + .returning(); + if (!oauthIntegration || !patIntegration) throw new Error('Expected integrations'); + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: oauthIntegration.id, + authorized_by_user_id: user.id, + provider_subject_id: '551', + provider_subject_login: 'oauth-user', + provider_base_url: providerBaseUrl, + access_token_encrypted: 'oauth-envelope', + refresh_token_encrypted: 'refresh-envelope', + }); + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: patIntegration.id, + token_encrypted: 'pat-envelope', + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: providerBaseUrl, + authorized_by_user_id: user.id, + provider_metadata: {}, + }); + + const audit = await runGitLabCredentialMigration(); + + expect(audit.counts.credentialProfileMismatches).toBe(0); + expect(audit.counts.providerMetadataMismatches).toBe(0); + expect(audit.integrationIds).toEqual([]); + }); + + it('reports duplicate OAuth and PAT primary credentials without mutating either', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: '606', + platform_account_login: 'oauth-user', + integration_status: 'active', + metadata: { + access_token: 'oauth-access-secret', + refresh_token: 'oauth-refresh-secret', + gitlab_instance_url: providerBaseUrl, + auth_type: 'oauth', + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: integration.id, + authorized_by_user_id: user.id, + provider_subject_id: '606', + provider_subject_login: 'oauth-user', + provider_base_url: providerBaseUrl, + access_token_encrypted: 'oauth-envelope', + refresh_token_encrypted: 'refresh-envelope', + }); + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: integration.id, + token_encrypted: 'pat-envelope', + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: providerBaseUrl, + authorized_by_user_id: user.id, + provider_metadata: {}, + }); + + const audit = await runGitLabCredentialMigration(); + expect(audit.counts.crossTablePrimaryCredentialDuplicates).toBe(1); + const oauthBefore = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)); + + const backfill = await runGitLabCredentialMigration({ mode: 'backfill', apply: true }); + expect(backfill.mutatedIntegrations).toBe(0); + await expect( + db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)) + ).resolves.toEqual(oauthBefore); + }); + + it('reports legacy OAuth tokens without provider identity as unmappable', async () => { + const user = await insertTestUser(); + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + integration_status: 'suspended', + metadata: { + access_token: 'oauth-access-secret', + refresh_token: 'oauth-refresh-secret', + auth_type: 'oauth', + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + + const audit = await runGitLabCredentialMigration(); + + expect(audit.counts.oauthMissingCredentials).toBe(1); + expect(audit.counts.unmappableLegacyEntries).toBe(1); + expect(audit.integrationIds).toEqual([integration.id]); + }); + + it('requires an explicit passing private-key audit assertion before scrub', async () => { + await expect( + runGitLabCredentialMigration({ + mode: 'scrub', + apply: true, + }) + ).rejects.toThrow('private-key audit assertion'); + }); + + it('reports an unpaired self-hosted OAuth client credential as unmappable', async () => { + const user = await insertTestUser(); + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: '650', + platform_account_login: 'oauth-user', + metadata: { + access_token: 'oauth-access-secret', + refresh_token: 'oauth-refresh-secret', + client_secret: 'unpaired-client-secret', + auth_type: 'oauth', + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + + const audit = await runGitLabCredentialMigration(); + + expect(audit.counts.unmappableLegacyEntries).toBe(1); + expect(audit.integrationIds).toEqual([integration.id]); + }); + + it('keeps backfill read-only without --apply', async () => { + const user = await insertTestUser(); + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: '707', + platform_account_login: 'pat-user', + metadata: { access_token: 'pat-secret', auth_type: 'pat' }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + + const result = await runGitLabCredentialMigration({ mode: 'backfill' }); + + expect(result.applied).toBe(false); + await expect( + db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)) + ).resolves.toEqual([]); + }); + + it('never updates an existing encrypted row when legacy metadata differs', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: '808', + platform_account_login: 'pat-user', + metadata: { + access_token: 'old-pat-secret', + gitlab_instance_url: providerBaseUrl, + auth_type: 'pat', + }, + }) + .returning(); + if (!integration) throw new Error('Expected integration'); + await runGitLabCredentialMigration({ mode: 'backfill', apply: true }); + const [firstCredential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + if (!firstCredential) throw new Error('Expected first credential'); + await db + .update(platform_integrations) + .set({ + metadata: { + access_token: 'new-pat-secret', + gitlab_instance_url: providerBaseUrl, + auth_type: 'pat', + }, + }) + .where(eq(platform_integrations.id, integration.id)); + + await runGitLabCredentialMigration({ mode: 'backfill', apply: true }); + + const [unchanged] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + if (!unchanged) throw new Error('Expected encrypted credential'); + expect(unchanged).toEqual(firstCredential); + expect( + decryptKeyedEnvelope( + unchanged.token_encrypted, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId: TEST_KEY_ID, privateKeyPem: testKeyPair.privateKey } }, + buildGitLabPersonalAccessTokenAad({ + credentialId: unchanged.id, + integrationId: integration.id, + providerBaseUrl, + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + credentialVersion: 1, + }) + ) + ).toBe('old-pat-secret'); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/gitlab/credential-migration.ts b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration.ts new file mode 100644 index 0000000000..2eef3a171e --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/credential-migration.ts @@ -0,0 +1,305 @@ +import 'server-only'; + +import { db } from '@/lib/drizzle'; +import { + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { and, asc, eq, gt, isNull } from 'drizzle-orm'; +import { + auditGitLabCredentialProfiles, + emptyGitLabCredentialAuditCounts, + hasBlockingGitLabCredentialAuditIssues, + type GitLabCredentialAuditCounts, +} from './credential-migration-audit'; +import { + countLegacySecretFields, + GitLabLegacyMetadataSchema, + hasTokenBearingLegacyMetadata, +} from './credential-migration-legacy'; +import { backfillMissingGitLabCredentials } from './credential-migration-backfill'; +import { normalizeGitLabInstanceUrl } from './instance-url'; +import { + mutateGitLabMetadataInTransaction, + readGitLabMetadataInTransaction, +} from './metadata-mutation'; + +export type GitLabCredentialMigrationMode = 'audit' | 'backfill' | 'scrub'; + +export type GitLabCredentialMigrationOptions = { + mode?: GitLabCredentialMigrationMode; + apply?: boolean; + batchSize?: number; + privateAuditPassed?: boolean; +}; + +export type GitLabCredentialMigrationResult = { + mode: GitLabCredentialMigrationMode; + applied: boolean; + scannedIntegrations: number; + mutatedIntegrations: number; + counts: GitLabCredentialAuditCounts; + integrationIds: string[]; +}; + +export async function runGitLabCredentialMigration( + options: GitLabCredentialMigrationOptions = {} +): Promise { + const mode = options.mode ?? 'audit'; + const batchSize = options.batchSize ?? 100; + if (!Number.isInteger(batchSize) || batchSize <= 0) { + throw new Error('GitLab credential migration batch size must be a positive integer'); + } + if (mode === 'scrub' && options.apply === true) { + if (options.privateAuditPassed !== true) { + throw new Error('GitLab credential scrub requires a passing private-key audit assertion'); + } + const audit = await runGitLabCredentialMigration({ mode: 'audit', batchSize }); + if (hasBlockingGitLabCredentialAuditIssues(audit.counts)) { + throw new Error('GitLab credential scrub blocked by unresolved public audit issues'); + } + } + + const result: GitLabCredentialMigrationResult = { + mode, + applied: false, + scannedIntegrations: 0, + mutatedIntegrations: 0, + counts: emptyGitLabCredentialAuditCounts(), + integrationIds: [], + }; + const issueIntegrationIds = new Set(); + const mutationsEnabled = options.apply === true && mode !== 'audit'; + result.applied = mutationsEnabled; + let cursor: string | undefined; + + while (true) { + const batch = await db + .select({ id: platform_integrations.id }) + .from(platform_integrations) + .where( + cursor + ? and(eq(platform_integrations.platform, 'gitlab'), gt(platform_integrations.id, cursor)) + : eq(platform_integrations.platform, 'gitlab') + ) + .orderBy(asc(platform_integrations.id)) + .limit(batchSize); + if (batch.length === 0) break; + + for (const { id: integrationId } of batch) { + result.scannedIntegrations += 1; + const mutated = await db.transaction(async tx => { + let rawMetadata: Record; + try { + rawMetadata = await readGitLabMetadataInTransaction(tx, integrationId); + } catch { + if (mode === 'scrub' && mutationsEnabled) { + throw new Error('GitLab credential scrub blocked by locked public audit'); + } + result.counts.malformedMetadata += 1; + result.counts.unmappableLegacyEntries += 1; + issueIntegrationIds.add(integrationId); + return false; + } + result.counts.legacySecretFields += countLegacySecretFields(rawMetadata); + if (hasTokenBearingLegacyMetadata(rawMetadata)) { + result.counts.legacyTokenBearingIntegrations += 1; + } + const metadataResult = GitLabLegacyMetadataSchema.safeParse(rawMetadata); + if (!metadataResult.success) { + if (mode === 'scrub' && mutationsEnabled) { + throw new Error('GitLab credential scrub blocked by locked public audit'); + } + result.counts.malformedMetadata += 1; + result.counts.unmappableLegacyEntries += 1; + issueIntegrationIds.add(integrationId); + return false; + } + const metadata = metadataResult.data; + const [integration] = await tx + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integrationId)) + .limit(1); + if (!integration) throw new Error('GitLab integration not found'); + if ( + Boolean(integration.owned_by_user_id) === Boolean(integration.owned_by_organization_id) + ) { + result.counts.unmappableLegacyEntries += 1; + issueIntegrationIds.add(integrationId); + return false; + } + let providerBaseUrl: string; + try { + providerBaseUrl = normalizeGitLabInstanceUrl(metadata.gitlab_instance_url); + } catch { + if (mode === 'scrub' && mutationsEnabled) { + throw new Error('GitLab credential scrub blocked by locked public audit'); + } + result.counts.malformedMetadata += 1; + result.counts.unmappableLegacyEntries += 1; + issueIntegrationIds.add(integrationId); + return false; + } + const projectTokens = metadata.project_tokens ?? {}; + + const [oauthCredential] = await tx + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integrationId)) + .limit(1); + const [patCredential] = await tx + .select() + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, integrationId), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ) + .limit(1); + const accessCredentials = await tx + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integrationId)); + const projectCredentialIds = new Set( + accessCredentials.flatMap(row => + row.provider_resource_id === null ? [] : [row.provider_resource_id] + ) + ); + + const credentialAudit = auditGitLabCredentialProfiles( + integration, + providerBaseUrl, + oauthCredential, + accessCredentials + ); + result.counts.credentialProfileMismatches += credentialAudit.profileMismatches; + result.counts.providerMetadataMismatches += credentialAudit.providerMetadataMismatches; + if ( + credentialAudit.profileMismatches > 0 || + credentialAudit.providerMetadataMismatches > 0 + ) { + issueIntegrationIds.add(integrationId); + } + if (oauthCredential && patCredential) { + result.counts.crossTablePrimaryCredentialDuplicates += 1; + issueIntegrationIds.add(integrationId); + } + + const oauthCredentialMissing = + metadata.auth_type === 'oauth' && Boolean(metadata.access_token) && !oauthCredential; + if (oauthCredentialMissing) { + result.counts.oauthMissingCredentials += 1; + issueIntegrationIds.add(integrationId); + } + const patCredentialMissing = + metadata.auth_type === 'pat' && Boolean(metadata.access_token) && !patCredential; + if (patCredentialMissing) { + result.counts.patMissingCredentials += 1; + issueIntegrationIds.add(integrationId); + } + const hasPrimaryLegacyMaterial = Boolean( + metadata.access_token || + metadata.refresh_token || + metadata.token_expires_at || + metadata.client_secret + ); + const hasSelfHostedClientCredential = + metadata.client_id !== undefined || metadata.client_secret !== undefined; + const selfHostedClientCredentialIsUnmappable = + hasSelfHostedClientCredential && + (metadata.auth_type !== 'oauth' || + (metadata.client_id === undefined) !== (metadata.client_secret === undefined)); + const primaryLegacyMaterialIsUnmappable = + hasPrimaryLegacyMaterial && + (selfHostedClientCredentialIsUnmappable || + metadata.auth_type === undefined || + (metadata.auth_type === 'oauth' && + (!metadata.access_token || + !metadata.refresh_token || + !integration.platform_account_id || + !integration.platform_account_login)) || + (metadata.auth_type === 'pat' && !metadata.access_token)); + if (primaryLegacyMaterialIsUnmappable) { + result.counts.unmappableLegacyEntries += 1; + issueIntegrationIds.add(integrationId); + } + const missingProjectCredentialIds = Object.keys(projectTokens).filter( + projectId => !projectCredentialIds.has(projectId) + ); + result.counts.projectMissingCredentials += missingProjectCredentialIds.length; + if (missingProjectCredentialIds.length > 0) issueIntegrationIds.add(integrationId); + const integrationTypeDisagrees = + metadata.auth_type !== undefined && metadata.auth_type !== integration.integration_type; + if (integrationTypeDisagrees) { + result.counts.integrationTypeDisagreements += 1; + issueIntegrationIds.add(integrationId); + } + + if (mode === 'backfill' && mutationsEnabled) { + const hasConflictingPrimary = + (metadata.auth_type === 'oauth' && patCredential !== undefined) || + (metadata.auth_type === 'pat' && oauthCredential !== undefined); + if (hasConflictingPrimary || (oauthCredential && patCredential)) return false; + const backfill = await backfillMissingGitLabCredentials(tx, integration, metadata, { + oauth: oauthCredential, + primaryAccess: patCredential, + access: accessCredentials, + }); + result.counts.unmappableLegacyEntries += backfill.unmappableProjects; + if (backfill.unmappableProjects > 0) issueIntegrationIds.add(integrationId); + return backfill.mutated; + } + if (mode === 'scrub' && mutationsEnabled) { + if ( + credentialAudit.profileMismatches > 0 || + credentialAudit.providerMetadataMismatches > 0 || + (oauthCredential !== undefined && patCredential !== undefined) || + oauthCredentialMissing || + patCredentialMissing || + primaryLegacyMaterialIsUnmappable || + missingProjectCredentialIds.length > 0 || + integrationTypeDisagrees + ) { + throw new Error('GitLab credential scrub blocked by locked public audit'); + } + const primaryCredential = + metadata.auth_type === 'oauth' + ? oauthCredential + : metadata.auth_type === 'pat' + ? patCredential + : undefined; + const projectCredentialsComplete = Object.keys(projectTokens).every(projectId => + projectCredentialIds.has(projectId) + ); + const deleteKeys: string[] = []; + if (primaryCredential) { + for (const key of [ + 'access_token', + 'refresh_token', + 'token_expires_at', + 'client_secret', + ]) { + if (key in rawMetadata) deleteKeys.push(key); + } + } + if (projectCredentialsComplete && 'project_tokens' in rawMetadata) { + deleteKeys.push('project_tokens'); + } + if (deleteKeys.length === 0) return false; + await mutateGitLabMetadataInTransaction(tx, integrationId, { delete: deleteKeys }); + return true; + } + return false; + }); + if (mutated) result.mutatedIntegrations += 1; + } + + cursor = batch.at(-1)?.id; + } + + result.integrationIds = [...issueIntegrationIds].sort(); + return result; +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/metadata-mutation.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/metadata-mutation.test.ts new file mode 100644 index 0000000000..53d50d4870 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/metadata-mutation.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from '@jest/globals'; +import type { DrizzleTransaction } from '@/lib/drizzle'; +import { mutateGitLabMetadataInTransaction } from './metadata-mutation'; + +function createTransactionHarness(initialMetadata: Record) { + const store = { metadata: initialMetadata }; + let lockTail = Promise.resolve(); + + async function transaction(callback: (tx: DrizzleTransaction) => Promise): Promise { + let releaseLock: (() => void) | undefined; + const tx: DrizzleTransaction = Object.create(null); + Object.assign(tx, { + execute: async () => { + const previousLock = lockTail; + lockTail = new Promise(resolve => { + releaseLock = resolve; + }); + await previousLock; + }, + select: () => ({ + from: () => ({ + where: () => ({ + limit: async () => [{ metadata: store.metadata }], + }), + }), + }), + update: () => ({ + set: ({ metadata }: { metadata: Record }) => ({ + where: async () => { + store.metadata = metadata; + }, + }), + }), + }); + + try { + return await callback(tx); + } finally { + releaseLock?.(); + } + } + + return { store, transaction }; +} + +describe('mutateGitLabMetadataInTransaction', () => { + it('preserves unrelated fields across concurrent mutations', async () => { + const { store, transaction } = createTransactionHarness({ auth_type: 'oauth' }); + + await Promise.all([ + transaction(tx => + mutateGitLabMetadataInTransaction(tx, 'integration-1', { + set: { webhook_secret: 'new-webhook-secret' }, + }) + ), + transaction(tx => + mutateGitLabMetadataInTransaction(tx, 'integration-1', { + set: { gitlab_instance_url: 'https://gitlab.example.com' }, + }) + ), + ]); + + expect(store.metadata).toEqual({ + auth_type: 'oauth', + webhook_secret: 'new-webhook-secret', + gitlab_instance_url: 'https://gitlab.example.com', + }); + }); + + it('keeps an explicitly deleted secret absent during a later unrelated mutation', async () => { + const { store, transaction } = createTransactionHarness({ + access_token: 'plaintext-secret', + webhook_secret: 'old-webhook-secret', + }); + + await transaction(tx => + mutateGitLabMetadataInTransaction(tx, 'integration-1', { + delete: ['access_token'], + }) + ); + await transaction(tx => + mutateGitLabMetadataInTransaction(tx, 'integration-1', { + set: { webhook_secret: 'new-webhook-secret' }, + }) + ); + + expect(store.metadata).toEqual({ webhook_secret: 'new-webhook-secret' }); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/gitlab/metadata-mutation.ts b/apps/web/src/lib/integrations/platforms/gitlab/metadata-mutation.ts new file mode 100644 index 0000000000..50f0542a39 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/metadata-mutation.ts @@ -0,0 +1,74 @@ +import 'server-only'; +import type { DrizzleTransaction } from '@/lib/drizzle'; +import { platform_integrations } from '@kilocode/db/schema'; +import { and, eq, sql } from 'drizzle-orm'; + +export type GitLabMetadataPatch = { + set?: Record; + delete?: readonly string[]; +}; + +type GitLabMetadataPatchInput = + | GitLabMetadataPatch + | ((currentMetadata: Readonly>) => GitLabMetadataPatch); + +function parseMetadata(metadata: unknown): Record { + if (metadata === null) return {}; + if (typeof metadata !== 'object' || Array.isArray(metadata)) { + throw new Error('GitLab integration metadata must be an object'); + } + return { ...metadata }; +} + +/** Acquires the GitLab integration lifecycle lock and returns freshly read metadata. */ +export async function readGitLabMetadataInTransaction( + tx: DrizzleTransaction, + integrationId: string +): Promise> { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`gitlab-integration:${integrationId}`}, 0))` + ); + + const [integration] = await tx + .select({ metadata: platform_integrations.metadata }) + .from(platform_integrations) + .where( + and(eq(platform_integrations.id, integrationId), eq(platform_integrations.platform, 'gitlab')) + ) + .limit(1); + + if (!integration) { + throw new Error('GitLab integration not found'); + } + + return parseMetadata(integration.metadata); +} + +/** + * Applies a GitLab metadata patch while holding the integration lifecycle lock. + * Callers must supply the transaction that contains their related integration writes. + */ +export async function mutateGitLabMetadataInTransaction( + tx: DrizzleTransaction, + integrationId: string, + patchInput: GitLabMetadataPatchInput +): Promise> { + const currentMetadata = await readGitLabMetadataInTransaction(tx, integrationId); + const patch = typeof patchInput === 'function' ? patchInput(currentMetadata) : patchInput; + const updatedMetadata = { ...currentMetadata, ...patch.set }; + for (const key of patch.delete ?? []) { + delete updatedMetadata[key]; + } + + await tx + .update(platform_integrations) + .set({ + metadata: updatedMetadata, + updated_at: new Date().toISOString(), + }) + .where( + and(eq(platform_integrations.id, integrationId), eq(platform_integrations.platform, 'gitlab')) + ); + + return updatedMetadata; +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/oauth-integration-writer.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/oauth-integration-writer.test.ts new file mode 100644 index 0000000000..9ea0ba029e --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/oauth-integration-writer.test.ts @@ -0,0 +1,496 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ + +import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; +import { generateKeyPairSync } from 'node:crypto'; +import { decryptKeyedEnvelope } from '@kilocode/encryption'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + buildGitLabOAuthCredentialAad, +} from '@kilocode/worker-utils/gitlab-credential'; +import { db } from '@/lib/drizzle'; +import { + kilocode_users, + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { eq } from 'drizzle-orm'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import { storeGitLabOAuthIntegration } from './oauth-integration-writer'; + +const testKeyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}); +const TEST_KEY_ID = 'platform-credential-key-v1'; +const mockCredentialEncryptionConfig: { + keyId: string | undefined; + publicKey: string | undefined; +} = { + keyId: TEST_KEY_ID, + publicKey: Buffer.from(testKeyPair.publicKey).toString('base64'), +}; + +jest.mock('@/lib/config.server', () => ({ + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID() { + return mockCredentialEncryptionConfig.keyId; + }, + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY() { + return mockCredentialEncryptionConfig.publicKey; + }, +})); + +describe('GitLab OAuth integration writer', () => { + beforeEach(() => { + mockCredentialEncryptionConfig.keyId = TEST_KEY_ID; + mockCredentialEncryptionConfig.publicKey = Buffer.from(testKeyPair.publicKey).toString( + 'base64' + ); + }); + + afterEach(async () => { + await db.delete(platform_oauth_credentials); + await db.delete(platform_access_token_credentials); + await db.delete(platform_integrations); + await db.delete(kilocode_users); + }); + + it('atomically stores a new OAuth integration with only encrypted credentials', async () => { + const user = await insertTestUser(); + const owner = { type: 'user', id: user.id } as const; + const input = { + owner, + authorizedByUserId: user.id, + providerBaseUrl: 'https://gitlab.example.com/root', + providerUser: { id: '12345', login: 'gitlab-user' }, + accessToken: 'oauth-access-secret', + refreshToken: 'oauth-refresh-secret', + accessTokenExpiresAt: '2030-01-01T00:00:00.000Z', + oauthClientId: 'custom-client-id', + oauthClientSecret: 'custom-client-secret', + scopes: ['api', 'read_user'], + repositories: [{ id: 42, name: 'project', full_name: 'group/project', private: true }], + }; + + const result = await storeGitLabOAuthIntegration(input); + + 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.oauth_client_secret_encrypted) { + throw new Error('Expected complete GitLab OAuth integration'); + } + + expect(result.instanceChanged).toBe(false); + expect(integration).toEqual( + expect.objectContaining({ + owned_by_user_id: user.id, + owned_by_organization_id: null, + platform: 'gitlab', + integration_type: 'oauth', + platform_installation_id: '12345', + platform_account_id: '12345', + platform_account_login: 'gitlab-user', + integration_status: 'active', + metadata: expect.objectContaining({ + gitlab_instance_url: input.providerBaseUrl, + client_id: input.oauthClientId, + auth_type: 'oauth', + }), + }) + ); + expect(integration.metadata).not.toHaveProperty('access_token'); + expect(integration.metadata).not.toHaveProperty('refresh_token'); + expect(integration.metadata).not.toHaveProperty('token_expires_at'); + expect(integration.metadata).not.toHaveProperty('client_secret'); + expect(credential).toEqual( + expect.objectContaining({ + authorized_by_user_id: user.id, + provider_subject_id: '12345', + provider_subject_login: 'gitlab-user', + provider_base_url: input.providerBaseUrl, + access_token_expires_at: expect.anything(), + credential_version: 1, + }) + ); + + const privateKeys = { + active: { + keyId: TEST_KEY_ID, + privateKeyPem: testKeyPair.privateKey, + }, + }; + const decrypt = (ciphertext: string, kind: 'access' | 'refresh' | 'oauth-client-secret') => + decryptKeyedEnvelope( + ciphertext, + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + privateKeys, + buildGitLabOAuthCredentialAad({ + credentialId: credential.id, + integrationId: integration.id, + providerBaseUrl: input.providerBaseUrl, + owner, + authorizedByUserId: user.id, + credentialVersion: credential.credential_version, + kind, + }) + ); + expect(decrypt(credential.access_token_encrypted, 'access')).toBe(input.accessToken); + if (!credential.refresh_token_encrypted) throw new Error('Expected encrypted refresh token'); + expect(decrypt(credential.refresh_token_encrypted, 'refresh')).toBe(input.refreshToken); + expect(decrypt(credential.oauth_client_secret_encrypted, 'oauth-client-secret')).toBe( + input.oauthClientSecret + ); + expect(JSON.stringify(credential)).not.toContain(input.accessToken); + expect(JSON.stringify(credential)).not.toContain(input.refreshToken); + expect(JSON.stringify(credential)).not.toContain(input.oauthClientSecret); + }); + + it('replaces the OAuth credential identity on a same-instance reconnect', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com/root'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: 'old-provider-id', + platform_account_login: 'old-login', + integration_status: 'active', + metadata: { + access_token: 'old-access', + refresh_token: 'old-refresh', + gitlab_instance_url: providerBaseUrl, + webhook_secret: 'preserved-webhook-secret', + configured_webhooks: ['42'], + auth_type: 'oauth', + }, + }) + .returning(); + if (!integration) throw new Error('Expected existing integration'); + const [oldCredential] = await db + .insert(platform_oauth_credentials) + .values({ + platform_integration_id: integration.id, + authorized_by_user_id: user.id, + provider_subject_id: 'old-provider-id', + provider_subject_login: 'old-login', + provider_base_url: providerBaseUrl, + access_token_encrypted: 'old-access-envelope', + refresh_token_encrypted: 'old-refresh-envelope', + }) + .returning(); + if (!oldCredential) throw new Error('Expected existing OAuth credential'); + + const result = await storeGitLabOAuthIntegration({ + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + providerBaseUrl, + providerUser: { id: 'new-provider-id', login: 'new-login' }, + accessToken: 'new-access-secret', + refreshToken: 'new-refresh-secret', + accessTokenExpiresAt: '2031-01-01T00:00:00.000Z', + oauthClientId: null, + oauthClientSecret: null, + scopes: ['api'], + repositories: null, + }); + + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + const credentials = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)); + + expect(result).toEqual({ integrationId: integration.id, instanceChanged: false }); + expect(credentials).toHaveLength(1); + expect(credentials[0]).toEqual( + expect.objectContaining({ + provider_subject_id: 'new-provider-id', + provider_subject_login: 'new-login', + provider_base_url: providerBaseUrl, + credential_version: 1, + }) + ); + expect(credentials[0]?.id).not.toBe(oldCredential.id); + expect(updatedIntegration).toEqual( + expect.objectContaining({ + integration_type: 'oauth', + platform_account_id: 'new-provider-id', + platform_account_login: 'new-login', + metadata: expect.objectContaining({ + webhook_secret: 'preserved-webhook-secret', + configured_webhooks: ['42'], + auth_type: 'oauth', + }), + }) + ); + expect(updatedIntegration.metadata).not.toHaveProperty('access_token'); + expect(updatedIntegration.metadata).not.toHaveProperty('refresh_token'); + }); + + it('replaces an integration PAT while preserving same-instance project credentials', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com/root'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: 'provider-id', + platform_account_login: 'provider-login', + integration_status: 'active', + metadata: { + access_token: 'old-pat', + gitlab_instance_url: providerBaseUrl, + webhook_secret: 'preserved-webhook-secret', + auth_type: 'pat', + }, + }) + .returning(); + if (!integration) throw new Error('Expected existing PAT integration'); + const [personalCredential, projectCredential] = await db + .insert(platform_access_token_credentials) + .values([ + { + platform_integration_id: integration.id, + token_encrypted: 'pat-envelope', + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: providerBaseUrl, + authorized_by_user_id: user.id, + provider_metadata: {}, + }, + { + platform_integration_id: integration.id, + token_encrypted: 'project-envelope', + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + provider_base_url: providerBaseUrl, + authorized_by_user_id: null, + provider_metadata: { providerCredentialId: '99', expiresOn: '2030-01-01' }, + }, + ]) + .returning(); + if (!personalCredential || !projectCredential) { + throw new Error('Expected PAT and project credentials'); + } + + await storeGitLabOAuthIntegration({ + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + providerBaseUrl, + providerUser: { id: 'provider-id', login: 'provider-login' }, + accessToken: 'oauth-access-secret', + refreshToken: 'oauth-refresh-secret', + accessTokenExpiresAt: '2031-01-01T00:00:00.000Z', + oauthClientId: null, + oauthClientSecret: null, + scopes: ['api'], + repositories: null, + }); + + const remainingAccessCredentials = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + expect(remainingAccessCredentials).toEqual([ + expect.objectContaining({ + id: projectCredential.id, + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + }), + ]); + expect(remainingAccessCredentials.map(row => row.id)).not.toContain(personalCredential.id); + await expect( + db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)) + ).resolves.toHaveLength(1); + }); + + it('deletes every access credential when the GitLab instance changes', async () => { + const user = await insertTestUser(); + const oldProviderBaseUrl = 'https://old-gitlab.example.com'; + const newProviderBaseUrl = 'https://new-gitlab.example.com/root'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'oauth', + platform_account_id: 'provider-id', + platform_account_login: 'provider-login', + integration_status: 'active', + metadata: { + access_token: 'old-access', + refresh_token: 'old-refresh', + gitlab_instance_url: oldProviderBaseUrl, + webhook_secret: 'old-webhook-secret', + configured_webhooks: ['42'], + project_tokens: { '42': { token: 'legacy-project-token' } }, + auth_type: 'oauth', + }, + }) + .returning(); + if (!integration) throw new Error('Expected existing integration'); + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: integration.id, + authorized_by_user_id: user.id, + provider_subject_id: 'provider-id', + provider_subject_login: 'provider-login', + provider_base_url: oldProviderBaseUrl, + access_token_encrypted: 'old-access-envelope', + refresh_token_encrypted: 'old-refresh-envelope', + }); + await db.insert(platform_access_token_credentials).values([ + { + platform_integration_id: integration.id, + token_encrypted: 'pat-envelope', + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: oldProviderBaseUrl, + authorized_by_user_id: user.id, + provider_metadata: {}, + }, + { + platform_integration_id: integration.id, + token_encrypted: 'project-envelope', + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + provider_base_url: oldProviderBaseUrl, + authorized_by_user_id: null, + provider_metadata: { providerCredentialId: '99', expiresOn: '2030-01-01' }, + }, + ]); + + const result = await storeGitLabOAuthIntegration({ + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + providerBaseUrl: newProviderBaseUrl, + providerUser: { id: 'new-provider-id', login: 'new-provider-login' }, + accessToken: 'new-access-secret', + refreshToken: 'new-refresh-secret', + accessTokenExpiresAt: '2031-01-01T00:00:00.000Z', + oauthClientId: 'new-client-id', + oauthClientSecret: 'new-client-secret', + scopes: ['api'], + repositories: null, + }); + + const [updatedIntegration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + const [updatedCredential] = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)); + expect(result.instanceChanged).toBe(true); + await expect( + db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)) + ).resolves.toEqual([]); + expect(updatedCredential?.provider_base_url).toBe(newProviderBaseUrl); + expect(updatedIntegration?.metadata).toEqual( + expect.not.objectContaining({ + configured_webhooks: expect.anything(), + project_tokens: expect.anything(), + }) + ); + expect(updatedIntegration?.metadata).toEqual( + expect.objectContaining({ + gitlab_instance_url: newProviderBaseUrl, + client_id: 'new-client-id', + }) + ); + expect(updatedIntegration?.metadata).not.toHaveProperty('client_secret'); + }); + + it('leaves existing state unchanged when credential encryption is unavailable', async () => { + const user = await insertTestUser(); + const providerBaseUrl = 'https://gitlab.example.com/root'; + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: user.id, + platform: 'gitlab', + integration_type: 'pat', + platform_account_id: 'provider-id', + platform_account_login: 'provider-login', + integration_status: 'active', + metadata: { + access_token: 'old-pat', + gitlab_instance_url: providerBaseUrl, + webhook_secret: 'old-webhook-secret', + auth_type: 'pat', + }, + }) + .returning(); + if (!integration) throw new Error('Expected existing PAT integration'); + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: integration.id, + token_encrypted: 'pat-envelope', + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: providerBaseUrl, + authorized_by_user_id: user.id, + provider_metadata: {}, + }); + const [integrationBefore] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + const accessCredentialsBefore = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + mockCredentialEncryptionConfig.publicKey = undefined; + + await expect( + storeGitLabOAuthIntegration({ + owner: { type: 'user', id: user.id }, + authorizedByUserId: user.id, + providerBaseUrl, + providerUser: { id: 'new-provider-id', login: 'new-provider-login' }, + accessToken: 'new-access-secret', + refreshToken: 'new-refresh-secret', + accessTokenExpiresAt: '2031-01-01T00:00:00.000Z', + oauthClientId: null, + oauthClientSecret: null, + scopes: ['api'], + repositories: null, + }) + ).rejects.toThrow('GitLab credential encryption is not configured'); + + await expect( + db.select().from(platform_integrations).where(eq(platform_integrations.id, integration.id)) + ).resolves.toEqual([integrationBefore]); + await expect( + db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)) + ).resolves.toEqual(accessCredentialsBefore); + await expect( + db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.id)) + ).resolves.toEqual([]); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/gitlab/oauth-integration-writer.ts b/apps/web/src/lib/integrations/platforms/gitlab/oauth-integration-writer.ts new file mode 100644 index 0000000000..4cdbce3217 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/gitlab/oauth-integration-writer.ts @@ -0,0 +1,232 @@ +import 'server-only'; + +import { randomBytes, randomUUID } from 'node:crypto'; +import { db } from '@/lib/drizzle'; +import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; +import type { Owner, PlatformRepository } from '@/lib/integrations/core/types'; +import { + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { and, eq, isNull } from 'drizzle-orm'; +import { encryptGitLabOAuthCredentials } from './credential-encryption'; +import { mutateGitLabMetadataInTransaction } from './metadata-mutation'; +import { isDefaultGitLabInstanceUrl, normalizeGitLabInstanceUrl } from './instance-url'; + +export type StoreGitLabOAuthIntegrationInput = { + owner: Owner; + authorizedByUserId: string; + providerBaseUrl: string; + providerUser: { id: string; login: string }; + accessToken: string; + refreshToken: string; + accessTokenExpiresAt: string; + oauthClientId: string | null; + oauthClientSecret: string | null; + scopes: string[]; + repositories: PlatformRepository[] | null; +}; + +export type StoreGitLabOAuthIntegrationResult = { + integrationId: string; + instanceChanged: boolean; +}; + +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); +} + +function generateWebhookSecret(): string { + return randomBytes(32).toString('hex'); +} + +function instanceUrlChanged(existingUrl: string | undefined, newUrl: string): boolean { + try { + return normalizeGitLabInstanceUrl(existingUrl) !== newUrl; + } catch { + return true; + } +} + +function readOptionalMetadataString( + metadata: Readonly>, + key: string +): string | undefined { + const value = metadata[key]; + if (value === undefined) return undefined; + if (typeof value !== 'string') throw new Error(`GitLab metadata ${key} must be a string`); + return value; +} + +export async function storeGitLabOAuthIntegration( + input: StoreGitLabOAuthIntegrationInput +): Promise { + const [existing] = await db + .select({ id: platform_integrations.id }) + .from(platform_integrations) + .where(and(ownerCondition(input.owner), eq(platform_integrations.platform, PLATFORM.GITLAB))) + .limit(1); + const integrationId = existing?.id ?? randomUUID(); + const credentialId = randomUUID(); + const credentialVersion = 1; + const encrypted = encryptGitLabOAuthCredentials({ + credentialId, + integrationId, + providerBaseUrl: input.providerBaseUrl, + owner: input.owner, + authorizedByUserId: input.authorizedByUserId, + credentialVersion, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + oauthClientSecret: input.oauthClientSecret, + }); + + if (existing) { + let changedInstance = false; + await db.transaction(async tx => { + await mutateGitLabMetadataInTransaction(tx, integrationId, currentMetadata => { + const currentInstanceUrl = readOptionalMetadataString( + currentMetadata, + 'gitlab_instance_url' + ); + const currentWebhookSecret = readOptionalMetadataString(currentMetadata, 'webhook_secret'); + changedInstance = instanceUrlChanged(currentInstanceUrl, input.providerBaseUrl); + + return { + set: { + webhook_secret: changedInstance + ? generateWebhookSecret() + : (currentWebhookSecret ?? generateWebhookSecret()), + auth_type: 'oauth', + ...(isDefaultGitLabInstanceUrl(input.providerBaseUrl) + ? {} + : { gitlab_instance_url: input.providerBaseUrl }), + ...(input.oauthClientId && input.oauthClientSecret + ? { client_id: input.oauthClientId } + : {}), + }, + delete: [ + 'access_token', + 'refresh_token', + 'token_expires_at', + 'client_secret', + ...(isDefaultGitLabInstanceUrl(input.providerBaseUrl) ? ['gitlab_instance_url'] : []), + ...(input.oauthClientId && input.oauthClientSecret ? [] : ['client_id']), + ...(changedInstance ? ['configured_webhooks', 'project_tokens'] : []), + ], + }; + }); + + await tx + .update(platform_integrations) + .set({ + integration_type: 'oauth', + platform_installation_id: input.providerUser.id, + platform_account_id: input.providerUser.id, + platform_account_login: input.providerUser.login, + scopes: input.scopes, + integration_status: INTEGRATION_STATUS.ACTIVE, + repositories: input.repositories, + updated_at: new Date().toISOString(), + }) + .where(eq(platform_integrations.id, integrationId)); + + if (changedInstance) { + await tx + .delete(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, integrationId)); + } else { + await tx + .delete(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, integrationId), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ); + } + + const credentialValues = { + authorized_by_user_id: input.authorizedByUserId, + provider_subject_id: input.providerUser.id, + provider_subject_login: input.providerUser.login, + provider_base_url: input.providerBaseUrl, + access_token_encrypted: encrypted.accessTokenEncrypted, + access_token_expires_at: input.accessTokenExpiresAt, + refresh_token_encrypted: encrypted.refreshTokenEncrypted, + refresh_token_expires_at: null, + oauth_client_secret_encrypted: encrypted.oauthClientSecretEncrypted, + revoked_at: null, + revocation_reason: null, + credential_version: credentialVersion, + updated_at: new Date().toISOString(), + }; + const [currentCredential] = await tx + .select({ id: platform_oauth_credentials.id }) + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integrationId)) + .limit(1); + if (currentCredential) { + await tx + .delete(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.id, currentCredential.id)); + } + await tx.insert(platform_oauth_credentials).values({ + id: credentialId, + platform_integration_id: integrationId, + ...credentialValues, + }); + }); + + return { integrationId, instanceChanged: changedInstance }; + } + + const metadata: Record = { + webhook_secret: generateWebhookSecret(), + auth_type: 'oauth', + }; + if (!isDefaultGitLabInstanceUrl(input.providerBaseUrl)) { + metadata.gitlab_instance_url = input.providerBaseUrl; + } + if (input.oauthClientId && input.oauthClientSecret) { + metadata.client_id = input.oauthClientId; + } + + await db.transaction(async tx => { + 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, + platform: PLATFORM.GITLAB, + integration_type: 'oauth', + platform_installation_id: input.providerUser.id, + platform_account_id: input.providerUser.id, + platform_account_login: input.providerUser.login, + permissions: null, + scopes: input.scopes, + repository_access: 'all', + integration_status: INTEGRATION_STATUS.ACTIVE, + repositories: input.repositories, + metadata, + installed_at: new Date().toISOString(), + }); + await tx.insert(platform_oauth_credentials).values({ + id: credentialId, + platform_integration_id: integrationId, + authorized_by_user_id: input.authorizedByUserId, + provider_subject_id: input.providerUser.id, + provider_subject_login: input.providerUser.login, + provider_base_url: input.providerBaseUrl, + access_token_encrypted: encrypted.accessTokenEncrypted, + access_token_expires_at: input.accessTokenExpiresAt, + refresh_token_encrypted: encrypted.refreshTokenEncrypted, + oauth_client_secret_encrypted: encrypted.oauthClientSecretEncrypted, + credential_version: credentialVersion, + }); + }); + + return { integrationId, instanceChanged: false }; +} diff --git a/apps/web/src/lib/integrations/platforms/gitlab/webhook-handlers/merge-request-handler.ts b/apps/web/src/lib/integrations/platforms/gitlab/webhook-handlers/merge-request-handler.ts index 43dce22a95..00ad4e59dd 100644 --- a/apps/web/src/lib/integrations/platforms/gitlab/webhook-handlers/merge-request-handler.ts +++ b/apps/web/src/lib/integrations/platforms/gitlab/webhook-handlers/merge-request-handler.ts @@ -38,6 +38,7 @@ import { getValidGitLabToken, } from '@/lib/integrations/gitlab-service'; import { APP_URL } from '@/lib/constants'; +import type { GitLabCredentialActor } from '../credential-broker-client'; const SUPERSEDED_BY_NEW_PUSH_REASON = 'Superseded by new push'; const DUPLICATE_MERGE_CONTINUATION_REASON = 'Superseded by duplicate merge-commit continuation'; @@ -107,6 +108,10 @@ export async function handleMergeRequestCodeReview( }); return NextResponse.json({ message: 'Integration missing user context' }, { status: 500 }); } + const credentialActor: GitLabCredentialActor = { + userId: owner.userId, + ...(owner.type === 'org' ? { organizationId: owner.id } : {}), + }; // 2. Check if code review agent is enabled for this owner (GitLab platform) const agentConfig = await getAgentConfigForOwner(owner, 'code_review', PLATFORM.GITLAB); @@ -184,7 +189,7 @@ export async function handleMergeRequestCodeReview( gitlab_instance_url?: string; } | null; const checkInstanceUrl = checkMetadata?.gitlab_instance_url || 'https://gitlab.com'; - const accessToken = await getValidGitLabToken(integrationForCheck); + const accessToken = await getValidGitLabToken(integrationForCheck, credentialActor); return isMergeCommit(accessToken, mr.source_project_id, headSha, checkInstanceUrl); }, }) @@ -206,6 +211,7 @@ export async function handleMergeRequestCodeReview( projectId: project.id, newHeadSha: headSha, reviewScope, + credentialActor, }); return NextResponse.json({ message: 'Skipped merge commit' }, { status: 200 }); @@ -248,6 +254,7 @@ export async function handleMergeRequestCodeReview( fullIntegration, instanceUrl, description: SUPERSEDED_BY_NEW_PUSH_REASON, + credentialActor, }); } @@ -293,7 +300,11 @@ export async function handleMergeRequestCodeReview( // 10. Post 👀 reaction and set commit status (using PrAT for bot identity) if (fullIntegration) { try { - const pratToken = await getOrCreateProjectAccessToken(fullIntegration, project.id); + const pratToken = await getOrCreateProjectAccessToken( + fullIntegration, + project.id, + credentialActor + ); logExceptInTest(`Got PrAT for project ${project.path_with_namespace}`, { projectId: project.id, }); @@ -448,6 +459,7 @@ async function setCancelledGitLabStatuses(args: { fullIntegration: PlatformIntegration; instanceUrl: string; description: string; + credentialActor: GitLabCredentialActor; }) { const gitlabCancelledReviews = args.cancelledReviews.flatMap(review => { if ( @@ -464,7 +476,11 @@ async function setCancelledGitLabStatuses(args: { const getProjectAccessToken = (platformProjectId: number) => { let token = projectAccessTokens.get(platformProjectId); if (!token) { - token = getOrCreateProjectAccessToken(args.fullIntegration, platformProjectId); + token = getOrCreateProjectAccessToken( + args.fullIntegration, + platformProjectId, + args.credentialActor + ); projectAccessTokens.set(platformProjectId, token); } return token; @@ -505,6 +521,7 @@ async function migrateInFlightReviewsToMergeCommitHead(args: { projectId: number; newHeadSha: string; reviewScope: ReviewScope; + credentialActor: GitLabCredentialActor; }) { try { const activeReviewIds = await findActiveReviewsForPR(args.reviewScope, args.newHeadSha); @@ -551,6 +568,7 @@ async function migrateInFlightReviewsToMergeCommitHead(args: { fullIntegration, instanceUrl, description: DUPLICATE_MERGE_CONTINUATION_REASON, + credentialActor: args.credentialActor, }); } } catch (duplicateCancelError) { @@ -562,7 +580,11 @@ async function migrateInFlightReviewsToMergeCommitHead(args: { } try { - const pratToken = await getOrCreateProjectAccessToken(fullIntegration, args.projectId); + const pratToken = await getOrCreateProjectAccessToken( + fullIntegration, + args.projectId, + args.credentialActor + ); const detailsUrl = `${APP_URL}/code-reviews/${reviewIdToContinue}`; await setCommitStatus( pratToken, diff --git a/apps/web/src/lib/user/index.test.ts b/apps/web/src/lib/user/index.test.ts index 07da7d0390..e9f9053f7b 100644 --- a/apps/web/src/lib/user/index.test.ts +++ b/apps/web/src/lib/user/index.test.ts @@ -603,7 +603,6 @@ describe('User', () => { .values([ { platform_integration_id: integration.id, - platform: 'bitbucket', authorized_by_user_id: user.id, provider_subject_id: '{bitbucket-user}', provider_subject_login: 'bitbucket-user', @@ -613,7 +612,6 @@ describe('User', () => { }, { 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', @@ -623,7 +621,6 @@ describe('User', () => { }, { 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', @@ -682,6 +679,136 @@ describe('User', () => { ).toHaveLength(1); }); + it('removes an organization GitLab PAT authorized by the deleted user without removing project tokens', async () => { + const authorizer = await insertTestUser(); + const organizationOwner = await insertTestUser(); + const organization = await createTestOrganization( + 'GitLab PAT Authorizer Cleanup Org', + organizationOwner.id, + 0 + ); + const [organizationIntegration, personalIntegration] = await db + .insert(platform_integrations) + .values([ + { + owned_by_organization_id: organization.id, + created_by_user_id: authorizer.id, + platform: 'gitlab', + integration_type: 'pat', + platform_installation_id: 'gitlab-org-pat-authorizer-cleanup', + platform_account_id: '1001', + platform_account_login: 'organization-authorizer', + integration_status: 'active', + }, + { + owned_by_user_id: authorizer.id, + created_by_user_id: authorizer.id, + platform: 'gitlab', + integration_type: 'pat', + platform_installation_id: 'gitlab-personal-pat-authorizer-cleanup', + platform_account_id: '1002', + platform_account_login: 'personal-authorizer', + integration_status: 'active', + }, + ]) + .returning(); + if (!organizationIntegration || !personalIntegration) { + throw new Error('Failed to create GitLab PAT integrations'); + } + + const [organizationPat, organizationProjectToken, personalPat, personalProjectToken] = + await db + .insert(platform_access_token_credentials) + .values([ + { + platform_integration_id: organizationIntegration.id, + token_encrypted: 'encrypted-organization-pat', + provider_credential_type: 'personal_access_token', + provider_base_url: 'https://gitlab.com', + authorized_by_user_id: authorizer.id, + provider_metadata: {}, + }, + { + platform_integration_id: organizationIntegration.id, + token_encrypted: 'encrypted-organization-project-token', + provider_credential_type: 'project_access_token', + provider_resource_id: '2001', + provider_base_url: 'https://gitlab.com', + provider_metadata: { + providerCredentialId: '3001', + expiresOn: '2027-07-13', + }, + }, + { + platform_integration_id: personalIntegration.id, + token_encrypted: 'encrypted-personal-pat', + provider_credential_type: 'personal_access_token', + provider_base_url: 'https://gitlab.com', + authorized_by_user_id: authorizer.id, + provider_metadata: {}, + }, + { + platform_integration_id: personalIntegration.id, + token_encrypted: 'encrypted-personal-project-token', + provider_credential_type: 'project_access_token', + provider_resource_id: '2002', + provider_base_url: 'https://gitlab.com', + provider_metadata: { + providerCredentialId: '3002', + expiresOn: '2027-07-13', + }, + }, + ]) + .returning(); + if (!organizationPat || !organizationProjectToken || !personalPat || !personalProjectToken) { + throw new Error('Failed to create GitLab PAT credentials'); + } + + await softDeleteUser(authorizer.id); + + 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_access_token_credentials) + .where(eq(platform_access_token_credentials.id, organizationPat.id)) + ).toHaveLength(0); + expect( + await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.id, organizationProjectToken.id)) + ).toHaveLength(1); + expect( + await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, personalIntegration.id)) + ).toHaveLength(0); + expect( + await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.id, personalPat.id)) + ).toHaveLength(0); + expect( + await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.id, personalProjectToken.id)) + ).toHaveLength(0); + }); + it('preserves an organization Workspace Access Token when its setup actor is deleted', async () => { const setupActor = await insertTestUser(); const organization = await createTestOrganization( diff --git a/apps/web/src/lib/user/index.ts b/apps/web/src/lib/user/index.ts index d915f86300..68444d35f7 100644 --- a/apps/web/src/lib/user/index.ts +++ b/apps/web/src/lib/user/index.ts @@ -38,6 +38,7 @@ import { auto_top_up_configs, platform_integrations, platform_oauth_credentials, + platform_access_token_credentials, byok_api_keys, agent_configs, webhook_events, @@ -863,6 +864,8 @@ export class SoftDeletePreconditionError extends Error { * - Recommendation dismissal actor references (nulled) * - platform_oauth_credentials (encrypted OAuth tokens and provider identity; * authorizations created by the user are removed, including organization grants) + * - Organization GitLab PAT credentials authorized by the user (parent integration suspended; + * project access-token credentials preserved) * - Various user-owned resources (platform_integrations, byok_api_keys, * agent_configs, webhook_events, code_indexing_*, source_embeddings, * cloud_agent_webhook_triggers, agent_environment_profiles, @@ -1105,6 +1108,47 @@ export async function softDeleteUser(userId: string) { await tx .delete(platform_oauth_credentials) .where(eq(platform_oauth_credentials.authorized_by_user_id, userId)); + + const authorizedGitLabPatIntegrationIds = tx + .select({ id: platform_access_token_credentials.platform_integration_id }) + .from(platform_access_token_credentials) + .innerJoin( + platform_integrations, + eq(platform_integrations.id, platform_access_token_credentials.platform_integration_id) + ) + .where( + and( + eq(platform_integrations.platform, 'gitlab'), + eq(platform_access_token_credentials.provider_credential_type, 'personal_access_token'), + eq(platform_access_token_credentials.authorized_by_user_id, userId), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ); + 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, authorizedGitLabPatIntegrationIds)); + await tx + .delete(platform_access_token_credentials) + .where( + and( + inArray( + platform_access_token_credentials.platform_integration_id, + tx + .select({ id: platform_integrations.id }) + .from(platform_integrations) + .where(eq(platform_integrations.platform, 'gitlab')) + ), + eq(platform_access_token_credentials.provider_credential_type, 'personal_access_token'), + eq(platform_access_token_credentials.authorized_by_user_id, userId), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ); + 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 index ebba6fc2cb..d0da43191e 100644 --- a/apps/web/src/routers/bitbucket-router.test.ts +++ b/apps/web/src/routers/bitbucket-router.test.ts @@ -74,7 +74,6 @@ async function insertPendingIntegration(organizationId: string, authorizedByUser 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', diff --git a/apps/web/src/routers/code-reviews-router.ts b/apps/web/src/routers/code-reviews-router.ts index 20a809a8ca..cdfb4b63e3 100644 --- a/apps/web/src/routers/code-reviews-router.ts +++ b/apps/web/src/routers/code-reviews-router.ts @@ -322,7 +322,9 @@ export const personalReviewAgentRouter = createTRPCRouter({ if (webhookSecret) { try { // Get a valid access token (handles refresh if expired) - const accessToken = await getValidGitLabToken(integration); + const accessToken = await getValidGitLabToken(integration, { + userId: ctx.user.id, + }); const selectedRepositoryIds = (input.selectedRepositoryIds ?? []).filter( (repositoryId): repositoryId is number => typeof repositoryId === 'number' diff --git a/apps/web/src/routers/code-reviews/code-reviews-router.ts b/apps/web/src/routers/code-reviews/code-reviews-router.ts index 04bee0a9af..4ab28582f3 100644 --- a/apps/web/src/routers/code-reviews/code-reviews-router.ts +++ b/apps/web/src/routers/code-reviews/code-reviews-router.ts @@ -31,9 +31,10 @@ import { getIntegrationById } from '@/lib/integrations/db/platform-integrations' import { createCheckRun, updateCheckRun } from '@/lib/integrations/platforms/github/adapter'; import { setCommitStatus } from '@/lib/integrations/platforms/gitlab/adapter'; import { + getValidGitLabProjectAccessToken, getValidGitLabToken, - getStoredProjectAccessToken, } from '@/lib/integrations/gitlab-service'; +import type { GitLabCredentialActor } from '@/lib/integrations/platforms/gitlab/credential-broker-client'; import { PLATFORM } from '@/lib/integrations/core/constants'; import { APP_URL } from '@/lib/constants'; import { CodeReviewPlatformSchema } from '@/lib/code-reviews/core/schemas'; @@ -75,7 +76,7 @@ import { isLocalCodeReviewDevelopmentEnabled } from '@/lib/config.server'; * would be a no-op for all subsequent status callbacks because `check_run_id` * was cleared during reset. */ -async function recreatePRGateCheck(review: CloudAgentCodeReview) { +async function recreatePRGateCheck(review: CloudAgentCodeReview, actor: GitLabCredentialActor) { const platform = CodeReviewPlatformSchema.parse(review.platform); if (!shouldPublishCodeReviewToProvider(review)) return; if (platform === PLATFORM.BITBUCKET || !review.platform_integration_id) return; @@ -126,10 +127,9 @@ async function recreatePRGateCheck(review: CloudAgentCodeReview) { `[retrigger] Created check run ${checkRunId} for ${review.repo_full_name}#${review.pr_number}` ); } else if (platform === PLATFORM.GITLAB) { - const storedPrat = review.platform_project_id - ? getStoredProjectAccessToken(integration, review.platform_project_id) - : null; - const accessToken = storedPrat ? storedPrat.token : await getValidGitLabToken(integration); + const accessToken = review.platform_project_id + ? await getValidGitLabProjectAccessToken(integration, review.platform_project_id, actor) + : await getValidGitLabToken(integration, actor); const metadata = integration.metadata as { gitlab_instance_url?: string } | null; const instanceUrl = metadata?.gitlab_instance_url || 'https://gitlab.com'; @@ -152,7 +152,7 @@ async function recreatePRGateCheck(review: CloudAgentCodeReview) { * Without this, cancelling a pending review leaves a stale queued/pending gate * that permanently blocks protected branches. */ -async function cancelPRGateCheck(review: CloudAgentCodeReview) { +async function cancelPRGateCheck(review: CloudAgentCodeReview, actor: GitLabCredentialActor) { const platform = CodeReviewPlatformSchema.parse(review.platform); if (!shouldPublishCodeReviewToProvider(review)) return; if (platform === PLATFORM.BITBUCKET || !review.platform_integration_id) return; @@ -184,10 +184,9 @@ async function cancelPRGateCheck(review: CloudAgentCodeReview) { `[cancel] Finalized check run for ${review.repo_full_name}#${review.pr_number}` ); } else if (platform === PLATFORM.GITLAB) { - const storedPrat = review.platform_project_id - ? getStoredProjectAccessToken(integration, review.platform_project_id) - : null; - const accessToken = storedPrat ? storedPrat.token : await getValidGitLabToken(integration); + const accessToken = review.platform_project_id + ? await getValidGitLabProjectAccessToken(integration, review.platform_project_id, actor) + : await getValidGitLabToken(integration, actor); const metadata = integration.metadata as { gitlab_instance_url?: string } | null; const instanceUrl = metadata?.gitlab_instance_url || 'https://gitlab.com'; @@ -413,6 +412,13 @@ export const codeReviewRouter = createTRPCRouter({ }); } + const credentialActor: GitLabCredentialActor = { + userId: ctx.user.id, + ...(review.owned_by_organization_id + ? { organizationId: review.owned_by_organization_id } + : {}), + }; + // Don't allow cancelling already completed/failed/cancelled/interrupted reviews if (['completed', 'failed', 'cancelled', 'interrupted'].includes(review.status)) { throw new TRPCError({ @@ -441,7 +447,7 @@ export const codeReviewRouter = createTRPCRouter({ ); await cancelCodeReview(input.reviewId); try { - await cancelPRGateCheck(review); + await cancelPRGateCheck(review, credentialActor); } catch (gateError) { logExceptInTest('[cancel] Failed to finalize PR gate check:', gateError); } @@ -457,7 +463,7 @@ export const codeReviewRouter = createTRPCRouter({ console.error('Worker cancel failed, updating DB directly:', workerError); await cancelCodeReview(input.reviewId); try { - await cancelPRGateCheck(review); + await cancelPRGateCheck(review, credentialActor); } catch (gateError) { logExceptInTest('[cancel] Failed to finalize PR gate check:', gateError); } @@ -471,7 +477,7 @@ export const codeReviewRouter = createTRPCRouter({ // For pending reviews (not yet dispatched to worker), update DB and finalize gate await cancelCodeReview(input.reviewId); try { - await cancelPRGateCheck(review); + await cancelPRGateCheck(review, credentialActor); } catch (gateError) { logExceptInTest('[cancel] Failed to finalize PR gate check:', gateError); } @@ -586,7 +592,10 @@ export const codeReviewRouter = createTRPCRouter({ // Re-create PR gate check so status callbacks can update it. try { - await recreatePRGateCheck(review); + await recreatePRGateCheck(review, { + userId: owner.userId, + ...(owner.type === 'org' ? { organizationId: owner.id } : {}), + }); } catch (gateError) { // Non-blocking — the review still retries even if the gate check fails logExceptInTest('[retrigger] Failed to re-create PR gate check:', gateError); diff --git a/apps/web/src/routers/gitlab-router.ts b/apps/web/src/routers/gitlab-router.ts index 7b6221ba6a..b7b66f3c1c 100644 --- a/apps/web/src/routers/gitlab-router.ts +++ b/apps/web/src/routers/gitlab-router.ts @@ -60,7 +60,7 @@ export const gitlabRouter = createTRPCRouter({ await ensureOrganizationAccess(ctx, input.organizationId, ['owner', 'billing_manager']); } const owner = resolveOwner(ctx, input.organizationId); - return gitlabService.connectWithPAT(owner, input.token, input.instanceUrl); + return gitlabService.connectWithPAT(owner, input.token, input.instanceUrl, ctx.user.id); }), /** @@ -133,7 +133,15 @@ export const gitlabRouter = createTRPCRouter({ } const owner = resolveOwner(ctx, input.organizationId); - const result = await gitlabService.listGitLabRepositories(owner, input.integrationId, true); + const result = await gitlabService.listGitLabRepositories( + owner, + input.integrationId, + { + userId: ctx.user.id, + ...(input.organizationId ? { organizationId: input.organizationId } : {}), + }, + true + ); return { success: true, @@ -155,7 +163,15 @@ export const gitlabRouter = createTRPCRouter({ await ensureOrganizationAccess(ctx, input.organizationId); } const owner = resolveOwner(ctx, input.organizationId); - return gitlabService.listGitLabRepositories(owner, input.integrationId, input.forceRefresh); + return gitlabService.listGitLabRepositories( + owner, + input.integrationId, + { + userId: ctx.user.id, + ...(input.organizationId ? { organizationId: input.organizationId } : {}), + }, + input.forceRefresh + ); }), listBranches: baseProcedure @@ -171,7 +187,15 @@ export const gitlabRouter = createTRPCRouter({ await ensureOrganizationAccess(ctx, input.organizationId); } const owner = resolveOwner(ctx, input.organizationId); - return gitlabService.listGitLabBranches(owner, input.integrationId, input.projectPath); + return gitlabService.listGitLabBranches( + owner, + input.integrationId, + { + userId: ctx.user.id, + ...(input.organizationId ? { organizationId: input.organizationId } : {}), + }, + input.projectPath + ); }), regenerateWebhookSecret: baseProcedure diff --git a/apps/web/src/routers/organizations/organization-bitbucket-router.test.ts b/apps/web/src/routers/organizations/organization-bitbucket-router.test.ts index 7d90da5042..70d607ac2e 100644 --- a/apps/web/src/routers/organizations/organization-bitbucket-router.test.ts +++ b/apps/web/src/routers/organizations/organization-bitbucket-router.test.ts @@ -177,7 +177,6 @@ async function insertPendingOAuthIntegration(organizationId: string, actorUserId 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', @@ -224,7 +223,6 @@ async function insertActiveOAuthIntegration(organizationId: string, actorUserId: 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', 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 267c8ca022..a3d710816f 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 @@ -103,6 +103,7 @@ const mockFetchGitHubRepositoriesForOrganization = jest.fn< const mockFetchGitLabRepositoriesForOrganization = jest.fn< ( organizationId: string, + actorUserId: string, forceRefresh: boolean ) => Promise<{ repositories: unknown[]; @@ -515,7 +516,7 @@ describe('organizationCloudAgentNextRouter helper procedures', () => { ['GitLab', 'listGitLabRepositories', mockFetchGitLabRepositoriesForOrganization], ] as const)( 'lists organization %s repositories without creating a runtime client', - async (_, method, fetchRepositories) => { + async (platform, method, fetchRepositories) => { const repositories = { repositories: [], integrationInstalled: true, @@ -528,7 +529,18 @@ describe('organizationCloudAgentNextRouter helper procedures', () => { caller[method]({ organizationId: ORGANIZATION_ID, forceRefresh: true }) ).resolves.toEqual(repositories); expect(mockEnsureOrganizationAccess).toHaveBeenCalledWith('member-user', ORGANIZATION_ID); - expect(fetchRepositories).toHaveBeenCalledWith(ORGANIZATION_ID, true); + if (platform === 'GitLab') { + expect(mockFetchGitLabRepositoriesForOrganization).toHaveBeenCalledWith( + ORGANIZATION_ID, + 'member-user', + true + ); + } else { + expect(mockFetchGitHubRepositoriesForOrganization).toHaveBeenCalledWith( + ORGANIZATION_ID, + true + ); + } expect(mockCreateCloudAgentNextClient).not.toHaveBeenCalled(); } ); 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 7a79a13e57..54ebf0a13e 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 @@ -665,9 +665,10 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({ errorMessage: z.string().optional(), }) ) - .query(async ({ input }) => { + .query(async ({ ctx, input }) => { const result = await fetchGitLabRepositoriesForOrganization( input.organizationId, + ctx.user.id, input.forceRefresh ); return { diff --git a/apps/web/src/routers/organizations/organization-code-reviews-router.ts b/apps/web/src/routers/organizations/organization-code-reviews-router.ts index 47d9347306..ba1254a42c 100644 --- a/apps/web/src/routers/organizations/organization-code-reviews-router.ts +++ b/apps/web/src/routers/organizations/organization-code-reviews-router.ts @@ -448,8 +448,12 @@ export const organizationReviewAgentRouter = createTRPCRouter({ forceRefresh: z.boolean().optional().default(false), }) ) - .query(async ({ input }) => { - return await fetchGitLabRepositoriesForOrganization(input.organizationId, input.forceRefresh); + .query(async ({ ctx, input }) => { + return await fetchGitLabRepositoriesForOrganization( + input.organizationId, + ctx.user.id, + input.forceRefresh + ); }), /** @@ -622,7 +626,10 @@ export const organizationReviewAgentRouter = createTRPCRouter({ if (webhookSecret) { try { // Get a valid access token (handles refresh if expired) - const accessToken = await getValidGitLabToken(integration); + const accessToken = await getValidGitLabToken(integration, { + userId: ctx.user.id, + organizationId: input.organizationId, + }); const selectedRepositoryIds = (input.selectedRepositoryIds ?? []).filter( (repositoryId): repositoryId is number => typeof repositoryId === 'number' diff --git a/apps/web/src/scripts/db/gitlab-credential-migration.ts b/apps/web/src/scripts/db/gitlab-credential-migration.ts new file mode 100644 index 0000000000..77b18ac37c --- /dev/null +++ b/apps/web/src/scripts/db/gitlab-credential-migration.ts @@ -0,0 +1,67 @@ +import { + runGitLabCredentialMigration, + type GitLabCredentialMigrationMode, +} from '@/lib/integrations/platforms/gitlab/credential-migration'; +import { getGitLabCredentialEncryptionPublicKeyInfo } from '@/lib/integrations/platforms/gitlab/credential-encryption'; + +const MODES: readonly GitLabCredentialMigrationMode[] = ['audit', 'backfill', 'scrub']; + +type ScriptOptions = { + mode: GitLabCredentialMigrationMode; + apply: boolean; + privateAuditPassed: boolean; + batchSize: number; +}; + +function parseChoice(value: string, choices: readonly T[], label: string): T { + const choice = choices.find(candidate => candidate === value); + if (!choice) throw new Error(`Invalid ${label}: ${value}`); + return choice; +} + +function parseOptions(args: string[]): ScriptOptions { + let mode: GitLabCredentialMigrationMode = 'audit'; + let apply = false; + let privateAuditPassed = false; + let batchSize = 100; + + for (const arg of args) { + if (!arg.startsWith('--')) { + mode = parseChoice(arg, MODES, 'migration mode'); + } else if (arg === '--apply') { + apply = true; + } else if (arg === '--private-audit-passed') { + privateAuditPassed = true; + } else if (arg.startsWith('--batch-size=')) { + batchSize = Number(arg.slice('--batch-size='.length)); + } else { + throw new Error(`Unknown GitLab credential migration argument: ${arg}`); + } + } + + if (!Number.isInteger(batchSize) || batchSize <= 0) { + throw new Error('GitLab credential migration batch size must be a positive integer'); + } + return { mode, apply, privateAuditPassed, batchSize }; +} + +/** + * Usage: pnpm script:run db gitlab-credential-migration [audit|backfill|scrub] + * [--apply] [--private-audit-passed] [--batch-size=100] + * Scrub requires: scrub --apply --private-audit-passed + */ +export async function run(...args: string[]): Promise { + const options = parseOptions(args); + const keyInfo = getGitLabCredentialEncryptionPublicKeyInfo(); + console.log( + JSON.stringify({ + event: 'gitlab_credential_migration_started', + ...options, + keyId: keyInfo.keyId, + publicKeySha256: keyInfo.publicKeySha256, + }) + ); + + const result = await runGitLabCredentialMigration(options); + console.log(JSON.stringify({ event: 'gitlab_credential_migration_completed', ...result })); +} diff --git a/packages/db/src/migrations/0186_milky_amazoness.sql b/packages/db/src/migrations/0186_milky_amazoness.sql new file mode 100644 index 0000000000..0e7bb42d12 --- /dev/null +++ b/packages/db/src/migrations/0186_milky_amazoness.sql @@ -0,0 +1,19 @@ +ALTER TABLE "platform_access_token_credentials" DROP CONSTRAINT "UQ_platform_access_token_credentials_platform_integration_id";--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ALTER COLUMN "provider_scopes" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ALTER COLUMN "provider_verified_at" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ALTER COLUMN "last_validated_at" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "platform_oauth_credentials" ALTER COLUMN "authorized_by_user_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "platform_oauth_credentials" ALTER COLUMN "refresh_token_encrypted" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD COLUMN "provider_resource_id" text;--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD COLUMN "provider_base_url" text;--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD COLUMN "authorized_by_user_id" text;--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD COLUMN "provider_metadata" jsonb;--> statement-breakpoint +ALTER TABLE "platform_oauth_credentials" ADD COLUMN "provider_base_url" text;--> statement-breakpoint +ALTER TABLE "platform_oauth_credentials" ADD COLUMN "oauth_client_secret_encrypted" text;--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD CONSTRAINT "platform_access_token_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_access_token_credentials_integration_level" ON "platform_access_token_credentials" USING btree ("platform_integration_id") WHERE "platform_access_token_credentials"."provider_resource_id" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_platform_access_token_credentials_resource" ON "platform_access_token_credentials" USING btree ("platform_integration_id","provider_credential_type","provider_resource_id") WHERE "platform_access_token_credentials"."provider_resource_id" is not null;--> statement-breakpoint +CREATE INDEX "IDX_platform_access_token_credentials_authorized_by_user_id" ON "platform_access_token_credentials" USING btree ("authorized_by_user_id");--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD CONSTRAINT "platform_access_token_credentials_credential_version_check" CHECK ("platform_access_token_credentials"."credential_version" > 0);--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD CONSTRAINT "platform_access_token_credentials_resource_id_check" CHECK ("platform_access_token_credentials"."provider_resource_id" IS NULL OR "platform_access_token_credentials"."provider_resource_id" <> '');--> statement-breakpoint +ALTER TABLE "platform_oauth_credentials" ADD CONSTRAINT "platform_oauth_credentials_credential_version_check" CHECK ("platform_oauth_credentials"."credential_version" > 0); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0186_snapshot.json b/packages/db/src/migrations/meta/0186_snapshot.json new file mode 100644 index 0000000000..8cb6267072 --- /dev/null +++ b/packages/db/src/migrations/meta/0186_snapshot.json @@ -0,0 +1,35579 @@ +{ + "id": "6e2d87c5-ebf3-4cae-946f-085a39c7b32b", + "prevId": "f79541c4-ede0-44d4-86d6-a1cf4a1fbf95", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_configs": { + "name": "agent_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "runtime_state": { + "name": "runtime_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_configs_org_id": { + "name": "IDX_agent_configs_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_owned_by_user_id": { + "name": "IDX_agent_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_agent_type": { + "name": "IDX_agent_configs_agent_type", + "columns": [ + { + "expression": "agent_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_platform": { + "name": "IDX_agent_configs_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_configs_owned_by_organization_id_organizations_id_fk": { + "name": "agent_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_configs_org_agent_platform": { + "name": "UQ_agent_configs_org_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "agent_type", + "platform" + ] + }, + "UQ_agent_configs_user_agent_platform": { + "name": "UQ_agent_configs_user_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_user_id", + "agent_type", + "platform" + ] + } + }, + "policies": {}, + "checkConstraints": { + "agent_configs_owner_check": { + "name": "agent_configs_owner_check", + "value": "(\n (\"agent_configs\".\"owned_by_user_id\" IS NOT NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_configs\".\"owned_by_user_id\" IS NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "agent_configs_agent_type_check": { + "name": "agent_configs_agent_type_check", + "value": "\"agent_configs\".\"agent_type\" IN ('code_review', 'auto_triage', 'auto_fix', 'security_scan')" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_agents": { + "name": "agent_environment_profile_agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_agents_profile_id": { + "name": "IDX_agent_env_profile_agents_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_agents", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_agents_profile_slug": { + "name": "UQ_agent_env_profile_agents_profile_slug", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_commands": { + "name": "agent_environment_profile_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_commands_profile_id": { + "name": "IDX_agent_env_profile_commands_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_commands_profile_sequence": { + "name": "UQ_agent_env_profile_commands_profile_sequence", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_kilo_commands": { + "name": "agent_environment_profile_kilo_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtask": { + "name": "subtask", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_kilo_cmds_profile_id": { + "name": "IDX_agent_env_profile_kilo_cmds_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_kilo_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_kilo_cmds_profile_name": { + "name": "UQ_agent_env_profile_kilo_cmds_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_mcp_servers": { + "name": "agent_environment_profile_mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_mcp_servers_profile_id": { + "name": "IDX_agent_env_profile_mcp_servers_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_mcp_servers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_mcp_servers_profile_name": { + "name": "UQ_agent_env_profile_mcp_servers_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_repo_bindings": { + "name": "agent_environment_profile_repo_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profile_repo_bindings_user": { + "name": "UQ_agent_env_profile_repo_bindings_user", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profile_repo_bindings_org": { + "name": "UQ_agent_env_profile_repo_bindings_org", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profile_repo_bindings_owner_check": { + "name": "agent_env_profile_repo_bindings_owner_check", + "value": "(\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_skills": { + "name": "agent_environment_profile_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_markdown": { + "name": "raw_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_skills_profile_id": { + "name": "IDX_agent_env_profile_skills_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_skills", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_skills_profile_name": { + "name": "UQ_agent_env_profile_skills_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_vars": { + "name": "agent_environment_profile_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_vars_profile_id": { + "name": "IDX_agent_env_profile_vars_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_vars", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_vars_profile_key": { + "name": "UQ_agent_env_profile_vars_profile_key", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profiles": { + "name": "agent_environment_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profiles_org_name": { + "name": "UQ_agent_env_profiles_org_name", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_name": { + "name": "UQ_agent_env_profiles_user_name", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_org_default": { + "name": "UQ_agent_env_profiles_org_default", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_default": { + "name": "UQ_agent_env_profiles_user_default", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_org_id": { + "name": "IDX_agent_env_profiles_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_user_id": { + "name": "IDX_agent_env_profiles_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_created_by_user_id": { + "name": "IDX_agent_env_profiles_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profiles_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profiles_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profiles_owner_check": { + "name": "agent_env_profiles_owner_check", + "value": "(\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.api_kind": { + "name": "api_kind", + "schema": "", + "columns": { + "api_kind_id": { + "name": "api_kind_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_api_kind": { + "name": "UQ_api_kind", + "columns": [ + { + "expression": "api_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_compress_log": { + "name": "api_request_compress_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_api_request_compress_log_created_at": { + "name": "idx_api_request_compress_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_log": { + "name": "api_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_request_id": { + "name": "vercel_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_request_log_created_at": { + "name": "idx_api_request_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_feedback": { + "name": "app_builder_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_status": { + "name": "preview_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_feedback_created_at": { + "name": "IDX_app_builder_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_kilo_user_id": { + "name": "IDX_app_builder_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_project_id": { + "name": "IDX_app_builder_feedback_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "app_builder_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "app_builder_feedback_project_id_app_builder_projects_id_fk": { + "name": "app_builder_feedback_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_project_sessions": { + "name": "app_builder_project_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "worker_version": { + "name": "worker_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v2'" + } + }, + "indexes": { + "IDX_app_builder_project_sessions_project_id": { + "name": "IDX_app_builder_project_sessions_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_project_sessions_project_id_app_builder_projects_id_fk": { + "name": "app_builder_project_sessions_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_project_sessions", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_app_builder_project_sessions_cloud_agent_session_id": { + "name": "UQ_app_builder_project_sessions_cloud_agent_session_id", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_projects": { + "name": "app_builder_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "git_repo_full_name": { + "name": "git_repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_platform_integration_id": { + "name": "git_platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_projects_created_by_user_id": { + "name": "IDX_app_builder_projects_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_user_id": { + "name": "IDX_app_builder_projects_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_organization_id": { + "name": "IDX_app_builder_projects_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_created_at": { + "name": "IDX_app_builder_projects_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_last_message_at": { + "name": "IDX_app_builder_projects_last_message_at", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_git_repo_integration": { + "name": "IDX_app_builder_projects_git_repo_integration", + "columns": [ + { + "expression": "git_repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"app_builder_projects\".\"git_repo_full_name\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_projects_owned_by_user_id_kilocode_users_id_fk": { + "name": "app_builder_projects_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_owned_by_organization_id_organizations_id_fk": { + "name": "app_builder_projects_owned_by_organization_id_organizations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_deployment_id_deployments_id_fk": { + "name": "app_builder_projects_deployment_id_deployments_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk": { + "name": "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "platform_integrations", + "columnsFrom": [ + "git_platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "app_builder_projects_owner_check": { + "name": "app_builder_projects_owner_check", + "value": "(\n (\"app_builder_projects\".\"owned_by_user_id\" IS NOT NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NULL) OR\n (\"app_builder_projects\".\"owned_by_user_id\" IS NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.app_min_versions": { + "name": "app_min_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ios_min_version": { + "name": "ios_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "android_min_version": { + "name": "android_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_reported_messages": { + "name": "app_reported_messages", + "schema": "", + "columns": { + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_reported_messages_cli_session_id_cli_sessions_session_id_fk": { + "name": "app_reported_messages_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "app_reported_messages", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_fix_tickets": { + "name": "auto_fix_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "triage_ticket_id": { + "name": "triage_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'label'" + }, + "review_comment_id": { + "name": "review_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_comment_body": { + "name": "review_comment_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "diff_hunk": { + "name": "diff_hunk", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_ref": { + "name": "pr_head_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_branch": { + "name": "pr_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_fix_tickets_repo_issue": { + "name": "UQ_auto_fix_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"trigger_source\" = 'label'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_fix_tickets_repo_review_comment": { + "name": "UQ_auto_fix_tickets_repo_review_comment", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "review_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"review_comment_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_org": { + "name": "IDX_auto_fix_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_user": { + "name": "IDX_auto_fix_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_status": { + "name": "IDX_auto_fix_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_created_at": { + "name": "IDX_auto_fix_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_triage_ticket_id": { + "name": "IDX_auto_fix_tickets_triage_ticket_id", + "columns": [ + { + "expression": "triage_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_session_id": { + "name": "IDX_auto_fix_tickets_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_fix_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_fix_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "triage_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk": { + "name": "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_fix_tickets_owner_check": { + "name": "auto_fix_tickets_owner_check", + "value": "(\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_fix_tickets_status_check": { + "name": "auto_fix_tickets_status_check", + "value": "\"auto_fix_tickets\".\"status\" IN ('pending', 'running', 'completed', 'failed', 'cancelled')" + }, + "auto_fix_tickets_classification_check": { + "name": "auto_fix_tickets_classification_check", + "value": "\"auto_fix_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'unclear')" + }, + "auto_fix_tickets_confidence_check": { + "name": "auto_fix_tickets_confidence_check", + "value": "\"auto_fix_tickets\".\"confidence\" >= 0 AND \"auto_fix_tickets\".\"confidence\" <= 1" + }, + "auto_fix_tickets_trigger_source_check": { + "name": "auto_fix_tickets_trigger_source_check", + "value": "\"auto_fix_tickets\".\"trigger_source\" IN ('label', 'review_comment')" + } + }, + "isRLSEnabled": false + }, + "public.auto_model": { + "name": "auto_model", + "schema": "", + "columns": { + "auto_model_id": { + "name": "auto_model_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_auto_model": { + "name": "UQ_auto_model", + "columns": [ + { + "expression": "auto_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_top_up_configs": { + "name": "auto_top_up_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_method_id": { + "name": "stripe_payment_method_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "last_auto_top_up_at": { + "name": "last_auto_top_up_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_top_up_configs_owned_by_user_id": { + "name": "UQ_auto_top_up_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_top_up_configs_owned_by_organization_id": { + "name": "UQ_auto_top_up_configs_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "auto_top_up_configs_owned_by_organization_id_organizations_id_fk": { + "name": "auto_top_up_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_top_up_configs_exactly_one_owner": { + "name": "auto_top_up_configs_exactly_one_owner", + "value": "(\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NULL) OR (\"auto_top_up_configs\".\"owned_by_user_id\" IS NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.auto_triage_tickets": { + "name": "auto_triage_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_duplicate": { + "name": "is_duplicate", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duplicate_of_ticket_id": { + "name": "duplicate_of_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "similarity_score": { + "name": "similarity_score", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "qdrant_point_id": { + "name": "qdrant_point_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "should_auto_fix": { + "name": "should_auto_fix", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "action_taken": { + "name": "action_taken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_metadata": { + "name": "action_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_triage_tickets_repo_issue": { + "name": "UQ_auto_triage_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_org": { + "name": "IDX_auto_triage_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_user": { + "name": "IDX_auto_triage_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_status": { + "name": "IDX_auto_triage_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_created_at": { + "name": "IDX_auto_triage_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_qdrant_point_id": { + "name": "IDX_auto_triage_tickets_qdrant_point_id", + "columns": [ + { + "expression": "qdrant_point_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owner_status_created": { + "name": "IDX_auto_triage_tickets_owner_status_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_user_status_created": { + "name": "IDX_auto_triage_tickets_user_status_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_repo_classification": { + "name": "IDX_auto_triage_tickets_repo_classification", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "classification", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_triage_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_triage_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "duplicate_of_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_triage_tickets_owner_check": { + "name": "auto_triage_tickets_owner_check", + "value": "(\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_triage_tickets_issue_type_check": { + "name": "auto_triage_tickets_issue_type_check", + "value": "\"auto_triage_tickets\".\"issue_type\" IN ('issue', 'pull_request')" + }, + "auto_triage_tickets_classification_check": { + "name": "auto_triage_tickets_classification_check", + "value": "\"auto_triage_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'duplicate', 'unclear')" + }, + "auto_triage_tickets_confidence_check": { + "name": "auto_triage_tickets_confidence_check", + "value": "\"auto_triage_tickets\".\"confidence\" >= 0 AND \"auto_triage_tickets\".\"confidence\" <= 1" + }, + "auto_triage_tickets_similarity_score_check": { + "name": "auto_triage_tickets_similarity_score_check", + "value": "\"auto_triage_tickets\".\"similarity_score\" >= 0 AND \"auto_triage_tickets\".\"similarity_score\" <= 1" + }, + "auto_triage_tickets_status_check": { + "name": "auto_triage_tickets_status_check", + "value": "\"auto_triage_tickets\".\"status\" IN ('pending', 'analyzing', 'actioned', 'failed', 'skipped')" + }, + "auto_triage_tickets_action_taken_check": { + "name": "auto_triage_tickets_action_taken_check", + "value": "\"auto_triage_tickets\".\"action_taken\" IN ('pr_created', 'comment_posted', 'closed_duplicate', 'needs_clarification')" + } + }, + "isRLSEnabled": false + }, + "public.bot_request_cloud_agent_sessions": { + "name": "bot_request_cloud_agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "bot_request_id": { + "name": "bot_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spawn_group_id": { + "name": "spawn_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlab_project": { + "name": "gitlab_project", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_step": { + "name": "callback_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message": { + "name": "final_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message_fetched_at": { + "name": "final_message_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "final_message_error": { + "name": "final_message_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "continuation_started_at": { + "name": "continuation_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_bot_request_cas_cloud_agent_session_id": { + "name": "UQ_bot_request_cas_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id": { + "name": "IDX_bot_request_cas_bot_request_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id_status": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id_status", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk": { + "name": "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk", + "tableFrom": "bot_request_cloud_agent_sessions", + "tableTo": "bot_requests", + "columnsFrom": [ + "bot_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_requests": { + "name": "bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_thread_id": { + "name": "platform_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_message_id": { + "name": "platform_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_bot_requests_created_at": { + "name": "IDX_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_created_by": { + "name": "IDX_bot_requests_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_organization_id": { + "name": "IDX_bot_requests_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_platform_integration_id": { + "name": "IDX_bot_requests_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_status": { + "name": "IDX_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_requests_created_by_kilocode_users_id_fk": { + "name": "bot_requests_created_by_kilocode_users_id_fk", + "tableFrom": "bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_organization_id_organizations_id_fk": { + "name": "bot_requests_organization_id_organizations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.byok_api_keys": { + "name": "byok_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "management_source": { + "name": "management_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_byok_api_keys_organization_id": { + "name": "IDX_byok_api_keys_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_kilo_user_id": { + "name": "IDX_byok_api_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_provider_id": { + "name": "IDX_byok_api_keys_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "byok_api_keys_organization_id_organizations_id_fk": { + "name": "byok_api_keys_organization_id_organizations_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "byok_api_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "byok_api_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_byok_api_keys_org_provider": { + "name": "UQ_byok_api_keys_org_provider", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider_id" + ] + }, + "UQ_byok_api_keys_user_provider": { + "name": "UQ_byok_api_keys_user_provider", + "nullsNotDistinct": false, + "columns": [ + "kilo_user_id", + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "byok_api_keys_management_source_check": { + "name": "byok_api_keys_management_source_check", + "value": "\"byok_api_keys\".\"management_source\" IN ('user', 'coding_plan')" + }, + "byok_api_keys_owner_check": { + "name": "byok_api_keys_owner_check", + "value": "(\n (\"byok_api_keys\".\"kilo_user_id\" IS NOT NULL AND \"byok_api_keys\".\"organization_id\" IS NULL) OR\n (\"byok_api_keys\".\"kilo_user_id\" IS NULL AND \"byok_api_keys\".\"organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cli_sessions": { + "name": "cli_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_mode": { + "name": "last_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_kilo_user_id": { + "name": "IDX_cli_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_created_at": { + "name": "IDX_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_updated_at": { + "name": "IDX_cli_sessions_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_organization_id": { + "name": "IDX_cli_sessions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_user_updated": { + "name": "IDX_cli_sessions_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_forked_from_cli_sessions_session_id_fk": { + "name": "cli_sessions_forked_from_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "forked_from" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_parent_session_id_cli_sessions_session_id_fk": { + "name": "cli_sessions_parent_session_id_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "parent_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_organization_id_organizations_id_fk": { + "name": "cli_sessions_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cli_sessions_cloud_agent_session_id_unique": { + "name": "cli_sessions_cloud_agent_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_sessions_v2": { + "name": "cli_sessions_v2", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "manual_config": { + "name": "manual_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "review_type": { + "name": "review_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author": { + "name": "pr_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author_github_id": { + "name": "pr_author_github_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_ref": { + "name": "head_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "dispatch_reservation_id": { + "name": "dispatch_reservation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'v1'" + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_used": { + "name": "repository_review_instructions_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "repository_review_instructions_ref": { + "name": "repository_review_instructions_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_truncated": { + "name": "repository_review_instructions_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previous_summary_body": { + "name": "previous_summary_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_summary_head_sha": { + "name": "previous_summary_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tokens_in": { + "name": "total_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens_out": { + "name": "total_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cost_musd": { + "name": "total_cost_musd", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"manual_config\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_code_reviews_active_provider_publisher": { + "name": "UQ_cloud_agent_code_reviews_active_provider_publisher", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"platform_integration_id\" IS NOT NULL\n AND \"cloud_agent_code_reviews\".\"status\" IN ('pending', 'queued', 'running')\n AND (\"cloud_agent_code_reviews\".\"manual_config\" IS NULL OR \"cloud_agent_code_reviews\".\"manual_config\"->>'outputMode' = 'provider')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_org_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_user_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_session_id": { + "name": "idx_cloud_agent_code_reviews_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_cli_session_id": { + "name": "idx_cloud_agent_code_reviews_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_status": { + "name": "idx_cloud_agent_code_reviews_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_repo": { + "name": "idx_cloud_agent_code_reviews_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_number": { + "name": "idx_cloud_agent_code_reviews_pr_number", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_created_at": { + "name": "idx_cloud_agent_code_reviews_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_author_github_id": { + "name": "idx_cloud_agent_code_reviews_pr_author_github_id", + "columns": [ + { + "expression": "pr_author_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk": { + "name": "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_reviews_owner_check": { + "name": "cloud_agent_code_reviews_owner_check", + "value": "(\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_feedback": { + "name": "cloud_agent_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "UQ_coding_plan_sub_live_user_provider": { + "name": "UQ_coding_plan_sub_live_user_provider", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_status": { + "name": "IDX_coding_plan_sub_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_renewal": { + "name": "IDX_coding_plan_sub_renewal", + "columns": [ + { + "expression": "credit_renewal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_inventory": { + "name": "IDX_coding_plan_sub_inventory", + "columns": [ + { + "expression": "key_inventory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_subscriptions_user_id_kilocode_users_id_fk": { + "name": "coding_plan_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk": { + "name": "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "coding_plan_key_inventory", + "columnsFrom": [ + "key_inventory_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk": { + "name": "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "byok_api_keys", + "columnsFrom": [ + "installed_byok_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_subscriptions_status_check": { + "name": "coding_plan_subscriptions_status_check", + "value": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due', 'canceled')" + }, + "coding_plan_subscriptions_live_access_check": { + "name": "coding_plan_subscriptions_live_access_check", + "value": "\"coding_plan_subscriptions\".\"status\" = 'canceled' OR \"coding_plan_subscriptions\".\"key_inventory_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_terms": { + "name": "coding_plan_terms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_terms_request": { + "name": "UQ_coding_plan_terms_request", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_terms_subscription": { + "name": "IDX_coding_plan_terms_subscription", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk": { + "name": "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "coding_plan_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_user_id_kilocode_users_id_fk": { + "name": "coding_plan_terms_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk": { + "name": "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_terms_kind_check": { + "name": "coding_plan_terms_kind_check", + "value": "\"coding_plan_terms\".\"kind\" IN ('activation', 'extension', 'renewal')" + } + }, + "isRLSEnabled": false + }, + "public.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.cost_insight_active_suggestions": { + "name": "cost_insight_active_suggestions", + "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 + }, + "suggestion_kind": { + "name": "suggestion_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggestion_key": { + "name": "suggestion_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta_label": { + "name": "cta_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta_href": { + "name": "cta_href", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_window_start": { + "name": "evidence_window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "evidence_window_end": { + "name": "evidence_window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "observed_microdollars": { + "name": "observed_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "benefit_label": { + "name": "benefit_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "benefit_detail": { + "name": "benefit_detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_by_user_id": { + "name": "dismissed_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()" + } + }, + "indexes": { + "UQ_cost_insight_active_suggestions_user_key": { + "name": "UQ_cost_insight_active_suggestions_user_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "suggestion_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_active_suggestions\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_active_suggestions_org_key": { + "name": "UQ_cost_insight_active_suggestions_org_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "suggestion_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_active_suggestions\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_active_suggestions_user_active": { + "name": "IDX_cost_insight_active_suggestions_user_active", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_active_suggestions\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_active_suggestions\".\"dismissed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_active_suggestions_org_active": { + "name": "IDX_cost_insight_active_suggestions_org_active", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_active_suggestions\".\"owned_by_organization_id\" IS NOT NULL AND \"cost_insight_active_suggestions\".\"dismissed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_active_suggestions_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_active_suggestions_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_active_suggestions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_active_suggestions_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_active_suggestions_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_active_suggestions", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_active_suggestions_dismissed_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_active_suggestions_dismissed_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_active_suggestions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "dismissed_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_active_suggestions_owner_check": { + "name": "cost_insight_active_suggestions_owner_check", + "value": "(\"cost_insight_active_suggestions\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_active_suggestions\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_active_suggestions\".\"owned_by_user_id\" IS NULL AND \"cost_insight_active_suggestions\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_active_suggestions_kind_check": { + "name": "cost_insight_active_suggestions_kind_check", + "value": "\"cost_insight_active_suggestions\".\"suggestion_kind\" IN ('coding_plan', 'kilo_pass')" + }, + "cost_insight_active_suggestions_key_check": { + "name": "cost_insight_active_suggestions_key_check", + "value": "\"cost_insight_active_suggestions\".\"suggestion_key\" ~ '^[0-9a-f]{64}$'" + }, + "cost_insight_active_suggestions_window_check": { + "name": "cost_insight_active_suggestions_window_check", + "value": "\"cost_insight_active_suggestions\".\"evidence_window_end\" > \"cost_insight_active_suggestions\".\"evidence_window_start\"" + }, + "cost_insight_active_suggestions_observed_positive_check": { + "name": "cost_insight_active_suggestions_observed_positive_check", + "value": "\"cost_insight_active_suggestions\".\"observed_microdollars\" > 0" + }, + "cost_insight_active_suggestions_observed_safe_check": { + "name": "cost_insight_active_suggestions_observed_safe_check", + "value": "\"cost_insight_active_suggestions\".\"observed_microdollars\" <= 9007199254740991" + }, + "cost_insight_active_suggestions_dismissed_by_check": { + "name": "cost_insight_active_suggestions_dismissed_by_check", + "value": "\"cost_insight_active_suggestions\".\"dismissed_at\" IS NOT NULL OR \"cost_insight_active_suggestions\".\"dismissed_by_user_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_evaluation_dirty_owners": { + "name": "cost_insight_evaluation_dirty_owners", + "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 + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "dirty_at": { + "name": "dirty_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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 + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cost_insight_evaluation_dirty_owners_user": { + "name": "UQ_cost_insight_evaluation_dirty_owners_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_evaluation_dirty_owners\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_evaluation_dirty_owners_org": { + "name": "UQ_cost_insight_evaluation_dirty_owners_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_evaluation_dirty_owners\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_evaluation_dirty_owners_claim": { + "name": "IDX_cost_insight_evaluation_dirty_owners_claim", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dirty_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_evaluation_dirty_owners_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_evaluation_dirty_owners_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_evaluation_dirty_owners", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_evaluation_dirty_owners_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_evaluation_dirty_owners_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_evaluation_dirty_owners", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_evaluation_dirty_owners_owner_check": { + "name": "cost_insight_evaluation_dirty_owners_owner_check", + "value": "(\"cost_insight_evaluation_dirty_owners\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_evaluation_dirty_owners\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_evaluation_dirty_owners\".\"owned_by_user_id\" IS NULL AND \"cost_insight_evaluation_dirty_owners\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_evaluation_dirty_owners_generation_check": { + "name": "cost_insight_evaluation_dirty_owners_generation_check", + "value": "\"cost_insight_evaluation_dirty_owners\".\"generation\" > 0 AND \"cost_insight_evaluation_dirty_owners\".\"generation\" <= 9007199254740991" + }, + "cost_insight_evaluation_dirty_owners_attempt_count_check": { + "name": "cost_insight_evaluation_dirty_owners_attempt_count_check", + "value": "\"cost_insight_evaluation_dirty_owners\".\"attempt_count\" >= 0" + }, + "cost_insight_evaluation_dirty_owners_claim_token_check": { + "name": "cost_insight_evaluation_dirty_owners_claim_token_check", + "value": "(\"cost_insight_evaluation_dirty_owners\".\"claimed_at\" IS NULL AND \"cost_insight_evaluation_dirty_owners\".\"claim_token\" IS NULL) OR (\"cost_insight_evaluation_dirty_owners\".\"claimed_at\" IS NOT NULL AND \"cost_insight_evaluation_dirty_owners\".\"claim_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_events": { + "name": "cost_insight_events", + "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 + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alert_kind": { + "name": "alert_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggestion_kind": { + "name": "suggestion_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_suggestion_id": { + "name": "active_suggestion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_cost_insight_events_user_occurred": { + "name": "IDX_cost_insight_events_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_events_org_occurred": { + "name": "IDX_cost_insight_events_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_events_occurred": { + "name": "IDX_cost_insight_events_occurred", + "columns": [ + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_events_user_dedupe": { + "name": "UQ_cost_insight_events_user_dedupe", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_events\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_events\".\"dedupe_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_events_org_dedupe": { + "name": "UQ_cost_insight_events_org_dedupe", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_events\".\"owned_by_organization_id\" IS NOT NULL AND \"cost_insight_events\".\"dedupe_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_events_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_events_active_suggestion_id_cost_insight_active_suggestions_id_fk": { + "name": "cost_insight_events_active_suggestion_id_cost_insight_active_suggestions_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "cost_insight_active_suggestions", + "columnsFrom": [ + "active_suggestion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_events_actor_user_id_kilocode_users_id_fk": { + "name": "cost_insight_events_actor_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_events_owner_check": { + "name": "cost_insight_events_owner_check", + "value": "(\"cost_insight_events\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_events\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_events\".\"owned_by_user_id\" IS NULL AND \"cost_insight_events\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_events_type_check": { + "name": "cost_insight_events_type_check", + "value": "\"cost_insight_events\".\"event_type\" IN ('config_changed', 'anomaly_alert', 'threshold_crossed', 'alert_reviewed', 'suggestion_created', 'suggestion_dismissed', 'disabled')" + }, + "cost_insight_events_alert_kind_check": { + "name": "cost_insight_events_alert_kind_check", + "value": "\"cost_insight_events\".\"alert_kind\" IN ('anomaly', 'threshold', 'threshold_7d', 'threshold_30d')" + }, + "cost_insight_events_suggestion_kind_check": { + "name": "cost_insight_events_suggestion_kind_check", + "value": "\"cost_insight_events\".\"suggestion_kind\" IN ('coding_plan', 'kilo_pass')" + }, + "cost_insight_events_alert_kind_presence_check": { + "name": "cost_insight_events_alert_kind_presence_check", + "value": "(\"cost_insight_events\".\"event_type\" IN ('anomaly_alert', 'threshold_crossed', 'alert_reviewed') AND \"cost_insight_events\".\"alert_kind\" IS NOT NULL) OR (\"cost_insight_events\".\"event_type\" NOT IN ('anomaly_alert', 'threshold_crossed', 'alert_reviewed') AND \"cost_insight_events\".\"alert_kind\" IS NULL)" + }, + "cost_insight_events_suggestion_kind_presence_check": { + "name": "cost_insight_events_suggestion_kind_presence_check", + "value": "(\"cost_insight_events\".\"event_type\" IN ('suggestion_created', 'suggestion_dismissed') AND \"cost_insight_events\".\"suggestion_kind\" IS NOT NULL) OR (\"cost_insight_events\".\"event_type\" NOT IN ('suggestion_created', 'suggestion_dismissed') AND \"cost_insight_events\".\"suggestion_kind\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_hourly_sweep_checkpoints": { + "name": "cost_insight_hourly_sweep_checkpoints", + "schema": "", + "columns": { + "job_name": { + "name": "job_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cycle_as_of": { + "name": "cycle_as_of", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cohort_created_before": { + "name": "cohort_created_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cursor_owner_type": { + "name": "cursor_owner_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_owner_id": { + "name": "cursor_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_completed_at": { + "name": "last_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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_hourly_sweep_job_name_check": { + "name": "cost_insight_hourly_sweep_job_name_check", + "value": "\"cost_insight_hourly_sweep_checkpoints\".\"job_name\" <> ''" + }, + "cost_insight_hourly_sweep_cursor_owner_type_check": { + "name": "cost_insight_hourly_sweep_cursor_owner_type_check", + "value": "\"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IS NULL OR \"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IN ('user', 'organization')" + }, + "cost_insight_hourly_sweep_cursor_check": { + "name": "cost_insight_hourly_sweep_cursor_check", + "value": "(\"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_id\" IS NULL) OR (\"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_type\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cursor_owner_id\" IS NOT NULL)" + }, + "cost_insight_hourly_sweep_lease_check": { + "name": "cost_insight_hourly_sweep_lease_check", + "value": "(\"cost_insight_hourly_sweep_checkpoints\".\"lease_token\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"lease_expires_at\" IS NULL) OR (\"cost_insight_hourly_sweep_checkpoints\".\"lease_token\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"lease_expires_at\" IS NOT NULL)" + }, + "cost_insight_hourly_sweep_cycle_check": { + "name": "cost_insight_hourly_sweep_cycle_check", + "value": "(\"cost_insight_hourly_sweep_checkpoints\".\"cycle_id\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cycle_as_of\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cohort_created_before\" IS NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"started_at\" IS NULL) OR (\"cost_insight_hourly_sweep_checkpoints\".\"cycle_id\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cycle_as_of\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"cohort_created_before\" IS NOT NULL AND \"cost_insight_hourly_sweep_checkpoints\".\"started_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_notification_deliveries": { + "name": "cost_insight_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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 + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "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_cost_insight_notification_deliveries_event_recipient_channel": { + "name": "UQ_cost_insight_notification_deliveries_event_recipient_channel", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_notification_deliveries_claim": { + "name": "IDX_cost_insight_notification_deliveries_claim", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_notification_deliveries_event": { + "name": "IDX_cost_insight_notification_deliveries_event", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_notification_deliveries_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_notification_deliveries_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_notification_deliveries", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cost_insight_notification_deliveries_recipient_user_id_kilocode_users_id_fk": { + "name": "cost_insight_notification_deliveries_recipient_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_notification_deliveries", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_notification_deliveries_channel_check": { + "name": "cost_insight_notification_deliveries_channel_check", + "value": "\"cost_insight_notification_deliveries\".\"channel\" = 'email'" + }, + "cost_insight_notification_deliveries_status_check": { + "name": "cost_insight_notification_deliveries_status_check", + "value": "\"cost_insight_notification_deliveries\".\"status\" IN ('pending', 'sending', 'sent', 'failed', 'skipped')" + }, + "cost_insight_notification_deliveries_attempt_count_check": { + "name": "cost_insight_notification_deliveries_attempt_count_check", + "value": "\"cost_insight_notification_deliveries\".\"attempt_count\" >= 0" + }, + "cost_insight_notification_deliveries_terminal_check": { + "name": "cost_insight_notification_deliveries_terminal_check", + "value": "(\"cost_insight_notification_deliveries\".\"status\" = 'sent' AND \"cost_insight_notification_deliveries\".\"sent_at\" IS NOT NULL) OR (\"cost_insight_notification_deliveries\".\"status\" <> 'sent' AND \"cost_insight_notification_deliveries\".\"sent_at\" IS NULL)" + }, + "cost_insight_notification_deliveries_failure_check": { + "name": "cost_insight_notification_deliveries_failure_check", + "value": "(\"cost_insight_notification_deliveries\".\"status\" = 'failed' AND \"cost_insight_notification_deliveries\".\"failed_at\" IS NOT NULL) OR (\"cost_insight_notification_deliveries\".\"status\" <> 'failed' AND \"cost_insight_notification_deliveries\".\"failed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_configs": { + "name": "cost_insight_owner_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 + }, + "spend_alerts_enabled": { + "name": "spend_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "anomaly_alerts_enabled": { + "name": "anomaly_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cost_suggestions_enabled": { + "name": "cost_suggestions_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "spend_threshold_microdollars": { + "name": "spend_threshold_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "spend_7_day_threshold_microdollars": { + "name": "spend_7_day_threshold_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "spend_30_day_threshold_microdollars": { + "name": "spend_30_day_threshold_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "spend_alerts_enabled_at": { + "name": "spend_alerts_enabled_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_cost_insight_owner_configs_user": { + "name": "UQ_cost_insight_owner_configs_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_configs\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_owner_configs_org": { + "name": "UQ_cost_insight_owner_configs_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_configs\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_configs_evaluation": { + "name": "IDX_cost_insight_owner_configs_evaluation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"cost_suggestions_enabled\" = TRUE", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_configs_user_active": { + "name": "IDX_cost_insight_owner_configs_user_active", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_configs\".\"owned_by_user_id\" IS NOT NULL AND (\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"cost_suggestions_enabled\" = TRUE)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_configs_org_active": { + "name": "IDX_cost_insight_owner_configs_org_active", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_configs\".\"owned_by_organization_id\" IS NOT NULL AND (\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"cost_suggestions_enabled\" = TRUE)", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_configs_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_owner_configs_owner_check": { + "name": "cost_insight_owner_configs_owner_check", + "value": "(\"cost_insight_owner_configs\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_configs\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_configs\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_configs\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_owner_configs_threshold_positive_check": { + "name": "cost_insight_owner_configs_threshold_positive_check", + "value": "\"cost_insight_owner_configs\".\"spend_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_threshold_microdollars\" > 0" + }, + "cost_insight_owner_configs_threshold_safe_check": { + "name": "cost_insight_owner_configs_threshold_safe_check", + "value": "\"cost_insight_owner_configs\".\"spend_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_threshold_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_configs_7_day_threshold_positive_check": { + "name": "cost_insight_owner_configs_7_day_threshold_positive_check", + "value": "\"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" > 0" + }, + "cost_insight_owner_configs_7_day_threshold_safe_check": { + "name": "cost_insight_owner_configs_7_day_threshold_safe_check", + "value": "\"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_7_day_threshold_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_configs_30_day_threshold_positive_check": { + "name": "cost_insight_owner_configs_30_day_threshold_positive_check", + "value": "\"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" > 0" + }, + "cost_insight_owner_configs_30_day_threshold_safe_check": { + "name": "cost_insight_owner_configs_30_day_threshold_safe_check", + "value": "\"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" IS NULL OR \"cost_insight_owner_configs\".\"spend_30_day_threshold_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_configs_enabled_at_check": { + "name": "cost_insight_owner_configs_enabled_at_check", + "value": "\"cost_insight_owner_configs\".\"spend_alerts_enabled\" = TRUE OR \"cost_insight_owner_configs\".\"spend_alerts_enabled_at\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_hour_driver_buckets": { + "name": "cost_insight_owner_hour_driver_buckets", + "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 + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "spend_category": { + "name": "spend_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "driver_key": { + "name": "driver_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_key": { + "name": "product_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_key": { + "name": "feature_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_or_plan_key": { + "name": "model_or_plan_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_microdollars": { + "name": "total_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spend_record_count": { + "name": "spend_record_count", + "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": { + "UQ_cost_insight_driver_buckets_user": { + "name": "UQ_cost_insight_driver_buckets_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "driver_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_driver_buckets\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_driver_buckets_org": { + "name": "UQ_cost_insight_driver_buckets_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "driver_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_driver_buckets\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_driver_buckets_hour": { + "name": "IDX_cost_insight_driver_buckets_hour", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_hour_driver_buckets_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_hour_driver_buckets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_hour_driver_buckets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_hour_driver_buckets_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_hour_driver_buckets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_hour_driver_buckets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_hour_driver_buckets_actor_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_hour_driver_buckets_actor_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_hour_driver_buckets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_driver_buckets_owner_check": { + "name": "cost_insight_driver_buckets_owner_check", + "value": "(\"cost_insight_owner_hour_driver_buckets\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_hour_driver_buckets\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_hour_driver_buckets\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_hour_driver_buckets\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_driver_buckets_hour_check": { + "name": "cost_insight_driver_buckets_hour_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"hour_start\" = date_trunc('hour', \"cost_insight_owner_hour_driver_buckets\".\"hour_start\", 'UTC')" + }, + "cost_insight_driver_buckets_category_check": { + "name": "cost_insight_driver_buckets_category_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"spend_category\" IN ('variable', 'scheduled')" + }, + "cost_insight_driver_buckets_source_check": { + "name": "cost_insight_driver_buckets_source_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"source\" IN ('ai_gateway', 'kiloclaw', 'coding_plan', 'other')" + }, + "cost_insight_driver_buckets_driver_key_check": { + "name": "cost_insight_driver_buckets_driver_key_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"driver_key\" ~ '^[0-9a-f]{64}$'" + }, + "cost_insight_driver_buckets_product_key_check": { + "name": "cost_insight_driver_buckets_product_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"product_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_feature_key_check": { + "name": "cost_insight_driver_buckets_feature_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"feature_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_model_key_check": { + "name": "cost_insight_driver_buckets_model_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"model_or_plan_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_provider_key_check": { + "name": "cost_insight_driver_buckets_provider_key_check", + "value": "char_length(\"cost_insight_owner_hour_driver_buckets\".\"provider_key\") BETWEEN 1 AND 128" + }, + "cost_insight_driver_buckets_amount_positive_check": { + "name": "cost_insight_driver_buckets_amount_positive_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"total_microdollars\" > 0" + }, + "cost_insight_driver_buckets_amount_safe_check": { + "name": "cost_insight_driver_buckets_amount_safe_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"total_microdollars\" <= 9007199254740991" + }, + "cost_insight_driver_buckets_count_positive_check": { + "name": "cost_insight_driver_buckets_count_positive_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"spend_record_count\" > 0" + }, + "cost_insight_driver_buckets_count_safe_check": { + "name": "cost_insight_driver_buckets_count_safe_check", + "value": "\"cost_insight_owner_hour_driver_buckets\".\"spend_record_count\" <= 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_hour_totals": { + "name": "cost_insight_owner_hour_totals", + "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 + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "spend_category": { + "name": "spend_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_microdollars": { + "name": "total_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spend_record_count": { + "name": "spend_record_count", + "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": { + "UQ_cost_insight_owner_hour_totals_user": { + "name": "UQ_cost_insight_owner_hour_totals_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_totals\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_owner_hour_totals_org": { + "name": "UQ_cost_insight_owner_hour_totals_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spend_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_hour_totals\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_hour_totals_hour": { + "name": "IDX_cost_insight_owner_hour_totals_hour", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_hour_totals_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_hour_totals_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_hour_totals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_hour_totals_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_hour_totals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_hour_totals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_owner_hour_totals_owner_check": { + "name": "cost_insight_owner_hour_totals_owner_check", + "value": "(\"cost_insight_owner_hour_totals\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_hour_totals\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_hour_totals\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_hour_totals\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_owner_hour_totals_hour_check": { + "name": "cost_insight_owner_hour_totals_hour_check", + "value": "\"cost_insight_owner_hour_totals\".\"hour_start\" = date_trunc('hour', \"cost_insight_owner_hour_totals\".\"hour_start\", 'UTC')" + }, + "cost_insight_owner_hour_totals_category_check": { + "name": "cost_insight_owner_hour_totals_category_check", + "value": "\"cost_insight_owner_hour_totals\".\"spend_category\" IN ('variable', 'scheduled')" + }, + "cost_insight_owner_hour_totals_amount_positive_check": { + "name": "cost_insight_owner_hour_totals_amount_positive_check", + "value": "\"cost_insight_owner_hour_totals\".\"total_microdollars\" > 0" + }, + "cost_insight_owner_hour_totals_amount_safe_check": { + "name": "cost_insight_owner_hour_totals_amount_safe_check", + "value": "\"cost_insight_owner_hour_totals\".\"total_microdollars\" <= 9007199254740991" + }, + "cost_insight_owner_hour_totals_count_positive_check": { + "name": "cost_insight_owner_hour_totals_count_positive_check", + "value": "\"cost_insight_owner_hour_totals\".\"spend_record_count\" > 0" + }, + "cost_insight_owner_hour_totals_count_safe_check": { + "name": "cost_insight_owner_hour_totals_count_safe_check", + "value": "\"cost_insight_owner_hour_totals\".\"spend_record_count\" <= 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_owner_states": { + "name": "cost_insight_owner_states", + "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 + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_event_id": { + "name": "active_anomaly_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_episode_id": { + "name": "active_anomaly_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_hour_start": { + "name": "active_anomaly_hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_snapshot": { + "name": "active_anomaly_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "active_anomaly_reviewed_at": { + "name": "active_anomaly_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "threshold_crossing_active": { + "name": "threshold_crossing_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_threshold_event_id": { + "name": "active_threshold_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_threshold_episode_id": { + "name": "active_threshold_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "threshold_crossing_started_at": { + "name": "threshold_crossing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_threshold_snapshot": { + "name": "active_threshold_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "threshold_reviewed_at": { + "name": "threshold_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "threshold_recovered_at": { + "name": "threshold_recovered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_crossing_active": { + "name": "rolling_7_day_threshold_crossing_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_rolling_7_day_threshold_event_id": { + "name": "active_rolling_7_day_threshold_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_rolling_7_day_threshold_episode_id": { + "name": "active_rolling_7_day_threshold_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_crossing_started_at": { + "name": "rolling_7_day_threshold_crossing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_rolling_7_day_threshold_snapshot": { + "name": "active_rolling_7_day_threshold_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_reviewed_at": { + "name": "rolling_7_day_threshold_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_7_day_threshold_recovered_at": { + "name": "rolling_7_day_threshold_recovered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_crossing_active": { + "name": "rolling_30_day_threshold_crossing_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_rolling_30_day_threshold_event_id": { + "name": "active_rolling_30_day_threshold_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_rolling_30_day_threshold_episode_id": { + "name": "active_rolling_30_day_threshold_episode_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_crossing_started_at": { + "name": "rolling_30_day_threshold_crossing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_rolling_30_day_threshold_snapshot": { + "name": "active_rolling_30_day_threshold_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_reviewed_at": { + "name": "rolling_30_day_threshold_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rolling_30_day_threshold_recovered_at": { + "name": "rolling_30_day_threshold_recovered_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_cost_insight_owner_states_user": { + "name": "UQ_cost_insight_owner_states_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_states\".\"owned_by_organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cost_insight_owner_states_org": { + "name": "UQ_cost_insight_owner_states_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cost_insight_owner_states\".\"owned_by_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_states_unreviewed_user": { + "name": "IDX_cost_insight_owner_states_unreviewed_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_states\".\"owned_by_user_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_owner_states_unreviewed_org": { + "name": "IDX_cost_insight_owner_states_unreviewed_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_owner_states\".\"owned_by_organization_id\" IS NOT NULL AND ((\"cost_insight_owner_states\".\"active_anomaly_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"active_anomaly_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL) OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_episode_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL))", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_owner_states_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_owner_states_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_states_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_owner_states_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_owner_states_active_anomaly_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_anomaly_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_anomaly_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_owner_states_active_threshold_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_threshold_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_threshold_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_owner_states_active_rolling_7_day_threshold_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_rolling_7_day_threshold_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_rolling_7_day_threshold_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_insight_owner_states_active_rolling_30_day_threshold_event_id_cost_insight_events_id_fk": { + "name": "cost_insight_owner_states_active_rolling_30_day_threshold_event_id_cost_insight_events_id_fk", + "tableFrom": "cost_insight_owner_states", + "tableTo": "cost_insight_events", + "columnsFrom": [ + "active_rolling_30_day_threshold_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_owner_states_owner_check": { + "name": "cost_insight_owner_states_owner_check", + "value": "(\"cost_insight_owner_states\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_owner_states\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_owner_states\".\"owned_by_user_id\" IS NULL AND \"cost_insight_owner_states\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_owner_states_anomaly_hour_check": { + "name": "cost_insight_owner_states_anomaly_hour_check", + "value": "\"cost_insight_owner_states\".\"active_anomaly_hour_start\" IS NULL OR \"cost_insight_owner_states\".\"active_anomaly_hour_start\" = date_trunc('hour', \"cost_insight_owner_states\".\"active_anomaly_hour_start\", 'UTC')" + }, + "cost_insight_owner_states_threshold_active_check": { + "name": "cost_insight_owner_states_threshold_active_check", + "value": "\"cost_insight_owner_states\".\"threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"active_threshold_episode_id\" IS NULL AND \"cost_insight_owner_states\".\"threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"active_threshold_snapshot\" IS NULL AND \"cost_insight_owner_states\".\"threshold_reviewed_at\" IS NULL)" + }, + "cost_insight_owner_states_7_day_threshold_active_check": { + "name": "cost_insight_owner_states_7_day_threshold_active_check", + "value": "\"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_7_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_7_day_threshold_episode_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_7_day_threshold_snapshot\" IS NULL AND \"cost_insight_owner_states\".\"rolling_7_day_threshold_reviewed_at\" IS NULL)" + }, + "cost_insight_owner_states_30_day_threshold_active_check": { + "name": "cost_insight_owner_states_30_day_threshold_active_check", + "value": "\"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_active\" = TRUE OR (\"cost_insight_owner_states\".\"active_rolling_30_day_threshold_event_id\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_30_day_threshold_episode_id\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_crossing_started_at\" IS NULL AND \"cost_insight_owner_states\".\"active_rolling_30_day_threshold_snapshot\" IS NULL AND \"cost_insight_owner_states\".\"rolling_30_day_threshold_reviewed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_rollup_coverage": { + "name": "cost_insight_rollup_coverage", + "schema": "", + "columns": { + "rollup_version": { + "name": "rollup_version", + "type": "smallint", + "primaryKey": true, + "notNull": true + }, + "live_capture_start_hour": { + "name": "live_capture_start_hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "coverage_start_hour": { + "name": "coverage_start_hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_reconciled_at": { + "name": "last_reconciled_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_rollup_coverage_version_check": { + "name": "cost_insight_rollup_coverage_version_check", + "value": "\"cost_insight_rollup_coverage\".\"rollup_version\" > 0" + }, + "cost_insight_rollup_coverage_live_hour_check": { + "name": "cost_insight_rollup_coverage_live_hour_check", + "value": "\"cost_insight_rollup_coverage\".\"live_capture_start_hour\" IS NULL OR \"cost_insight_rollup_coverage\".\"live_capture_start_hour\" = date_trunc('hour', \"cost_insight_rollup_coverage\".\"live_capture_start_hour\", 'UTC')" + }, + "cost_insight_rollup_coverage_start_hour_check": { + "name": "cost_insight_rollup_coverage_start_hour_check", + "value": "\"cost_insight_rollup_coverage\".\"coverage_start_hour\" IS NULL OR \"cost_insight_rollup_coverage\".\"coverage_start_hour\" = date_trunc('hour', \"cost_insight_rollup_coverage\".\"coverage_start_hour\", 'UTC')" + }, + "cost_insight_rollup_coverage_range_check": { + "name": "cost_insight_rollup_coverage_range_check", + "value": "\"cost_insight_rollup_coverage\".\"coverage_start_hour\" IS NULL OR (\"cost_insight_rollup_coverage\".\"live_capture_start_hour\" IS NOT NULL AND \"cost_insight_rollup_coverage\".\"coverage_start_hour\" <= \"cost_insight_rollup_coverage\".\"live_capture_start_hour\")" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_rollup_degraded_intervals": { + "name": "cost_insight_rollup_degraded_intervals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "start_hour": { + "name": "start_hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_hour_exclusive": { + "name": "end_hour_exclusive", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_at": { + "name": "detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_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_cost_insight_degraded_intervals_unresolved": { + "name": "IDX_cost_insight_degraded_intervals_unresolved", + "columns": [ + { + "expression": "start_hour", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "end_hour_exclusive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cost_insight_rollup_degraded_intervals\".\"resolved_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cost_insight_degraded_intervals_start_hour_check": { + "name": "cost_insight_degraded_intervals_start_hour_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"start_hour\" = date_trunc('hour', \"cost_insight_rollup_degraded_intervals\".\"start_hour\", 'UTC')" + }, + "cost_insight_degraded_intervals_end_hour_check": { + "name": "cost_insight_degraded_intervals_end_hour_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"end_hour_exclusive\" = date_trunc('hour', \"cost_insight_rollup_degraded_intervals\".\"end_hour_exclusive\", 'UTC')" + }, + "cost_insight_degraded_intervals_range_check": { + "name": "cost_insight_degraded_intervals_range_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"end_hour_exclusive\" > \"cost_insight_rollup_degraded_intervals\".\"start_hour\"" + }, + "cost_insight_degraded_intervals_resolution_check": { + "name": "cost_insight_degraded_intervals_resolution_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"resolved_at\" IS NULL OR \"cost_insight_rollup_degraded_intervals\".\"resolved_at\" >= \"cost_insight_rollup_degraded_intervals\".\"detected_at\"" + }, + "cost_insight_degraded_intervals_source_check": { + "name": "cost_insight_degraded_intervals_source_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"source\" IN ('ai_gateway', 'kiloclaw', 'coding_plan', 'other')" + }, + "cost_insight_degraded_intervals_reason_check": { + "name": "cost_insight_degraded_intervals_reason_check", + "value": "\"cost_insight_rollup_degraded_intervals\".\"reason\" IN ('capture_bypass', 'reconciliation_mismatch', 'late_source_data')" + } + }, + "isRLSEnabled": false + }, + "public.cost_insight_rollup_repairs": { + "name": "cost_insight_rollup_repairs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cost_insight_rollup_repairs_user_hour": { + "name": "IDX_cost_insight_rollup_repairs_user_hour", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_rollup_repairs_org_hour": { + "name": "IDX_cost_insight_rollup_repairs_org_hour", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cost_insight_rollup_repairs_claim": { + "name": "IDX_cost_insight_rollup_repairs_claim", + "columns": [ + { + "expression": "attempt_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_insight_rollup_repairs_usage_id_microdollar_usage_id_fk": { + "name": "cost_insight_rollup_repairs_usage_id_microdollar_usage_id_fk", + "tableFrom": "cost_insight_rollup_repairs", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cost_insight_rollup_repairs_owned_by_user_id_kilocode_users_id_fk": { + "name": "cost_insight_rollup_repairs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cost_insight_rollup_repairs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "cost_insight_rollup_repairs_owned_by_organization_id_organizations_id_fk": { + "name": "cost_insight_rollup_repairs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cost_insight_rollup_repairs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cost_insight_rollup_repairs_usage_id_unique": { + "name": "cost_insight_rollup_repairs_usage_id_unique", + "nullsNotDistinct": false, + "columns": [ + "usage_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "cost_insight_rollup_repairs_owner_check": { + "name": "cost_insight_rollup_repairs_owner_check", + "value": "(\"cost_insight_rollup_repairs\".\"owned_by_user_id\" IS NOT NULL AND \"cost_insight_rollup_repairs\".\"owned_by_organization_id\" IS NULL) OR (\"cost_insight_rollup_repairs\".\"owned_by_user_id\" IS NULL AND \"cost_insight_rollup_repairs\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "cost_insight_rollup_repairs_hour_check": { + "name": "cost_insight_rollup_repairs_hour_check", + "value": "\"cost_insight_rollup_repairs\".\"hour_start\" = date_trunc('hour', \"cost_insight_rollup_repairs\".\"hour_start\", 'UTC')" + }, + "cost_insight_rollup_repairs_generation_check": { + "name": "cost_insight_rollup_repairs_generation_check", + "value": "\"cost_insight_rollup_repairs\".\"generation\" > 0 AND \"cost_insight_rollup_repairs\".\"generation\" <= 9007199254740991" + }, + "cost_insight_rollup_repairs_attempt_count_check": { + "name": "cost_insight_rollup_repairs_attempt_count_check", + "value": "\"cost_insight_rollup_repairs\".\"attempt_count\" >= 0" + }, + "cost_insight_rollup_repairs_claim_token_check": { + "name": "cost_insight_rollup_repairs_claim_token_check", + "value": "(\"cost_insight_rollup_repairs\".\"claimed_at\" IS NULL AND \"cost_insight_rollup_repairs\".\"claim_token\" IS NULL) OR (\"cost_insight_rollup_repairs\".\"claimed_at\" IS NOT NULL AND \"cost_insight_rollup_repairs\".\"claim_token\" IS NOT NULL)" + } + }, + "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 + }, + "personal_account_disabled": { + "name": "personal_account_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kilocode_users_signup_ip_created_at": { + "name": "IDX_kilocode_users_signup_ip_created_at", + "columns": [ + { + "expression": "signup_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_at": { + "name": "IDX_kilocode_users_blocked_at", + "columns": [ + { + "expression": "blocked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_by_kilo_user_id": { + "name": "IDX_kilocode_users_blocked_by_kilo_user_id", + "columns": [ + { + "expression": "blocked_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_upstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_upstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_upstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_upstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_downstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_downstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_downstream_safety_identifier\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_vercel_downstream_safety_identifier": { + "name": "UQ_kilocode_users_vercel_downstream_safety_identifier", + "columns": [ + { + "expression": "vercel_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"vercel_downstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_normalized_email": { + "name": "IDX_kilocode_users_normalized_email", + "columns": [ + { + "expression": "normalized_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_email_domain": { + "name": "IDX_kilocode_users_email_domain", + "columns": [ + { + "expression": "email_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilocode_users_app_store_account_token_unique": { + "name": "kilocode_users_app_store_account_token_unique", + "nullsNotDistinct": false, + "columns": [ + "app_store_account_token" + ] + }, + "UQ_b1afacbcf43f2c7c4cb9f7e7faa": { + "name": "UQ_b1afacbcf43f2c7c4cb9f7e7faa", + "nullsNotDistinct": false, + "columns": [ + "google_user_email" + ] + } + }, + "policies": {}, + "checkConstraints": { + "blocked_reason_not_empty": { + "name": "blocked_reason_not_empty", + "value": "length(blocked_reason) > 0" + }, + "kilocode_users_can_manage_credits_requires_admin_check": { + "name": "kilocode_users_can_manage_credits_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_manage_credits\" OR \"kilocode_users\".\"is_admin\"" + } + }, + "isRLSEnabled": false + }, + "public.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'magic_link'" + } + }, + "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 + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_metadata": { + "name": "correlation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_audit_events_config": { + "name": "IDX_mcp_gateway_audit_events_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_grant": { + "name": "IDX_mcp_gateway_audit_events_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_audit_events\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_owner": { + "name": "IDX_mcp_gateway_audit_events_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_created_at": { + "name": "IDX_mcp_gateway_audit_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_audit_events_owner_scope": { + "name": "mcp_gateway_audit_events_owner_scope", + "value": "\"mcp_gateway_audit_events\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_audit_events_outcome": { + "name": "mcp_gateway_audit_events_outcome", + "value": "\"mcp_gateway_audit_events\".\"outcome\" IN ('success', 'failure', 'blocked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_codes": { + "name": "mcp_gateway_authorization_codes", + "schema": "", + "columns": { + "authorization_code_id": { + "name": "authorization_code_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_codes_code_hash": { + "name": "UQ_mcp_gateway_authorization_codes_code_hash", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_expires_at": { + "name": "IDX_mcp_gateway_authorization_codes_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_client": { + "name": "IDX_mcp_gateway_authorization_codes_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_grant": { + "name": "IDX_mcp_gateway_authorization_codes_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_codes\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_codes_owner_scope": { + "name": "mcp_gateway_authorization_codes_owner_scope", + "value": "\"mcp_gateway_authorization_codes\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_requests": { + "name": "mcp_gateway_authorization_requests", + "schema": "", + "columns": { + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_state_hash": { + "name": "request_state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "oauth_state": { + "name": "oauth_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_status": { + "name": "request_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_requests_state_hash": { + "name": "UQ_mcp_gateway_authorization_requests_state_hash", + "columns": [ + { + "expression": "request_state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_config": { + "name": "IDX_mcp_gateway_authorization_requests_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_grant": { + "name": "IDX_mcp_gateway_authorization_requests_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_requests\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_user": { + "name": "IDX_mcp_gateway_authorization_requests_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_expires_at": { + "name": "IDX_mcp_gateway_authorization_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_requests_owner_scope": { + "name": "mcp_gateway_authorization_requests_owner_scope", + "value": "\"mcp_gateway_authorization_requests\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_authorization_requests_status": { + "name": "mcp_gateway_authorization_requests_status", + "value": "\"mcp_gateway_authorization_requests\".\"request_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_config_secrets": { + "name": "mcp_gateway_config_secrets", + "schema": "", + "columns": { + "config_secret_id": { + "name": "config_secret_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_kind": { + "name": "secret_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_config_secrets_active_kind": { + "name": "UQ_mcp_gateway_config_secrets_active_kind", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_config_secrets\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_config_secrets_config": { + "name": "IDX_mcp_gateway_config_secrets_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_config_secrets", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_config_secrets_version_positive": { + "name": "mcp_gateway_config_secrets_version_positive", + "value": "\"mcp_gateway_config_secrets\".\"secret_version\" > 0" + }, + "mcp_gateway_config_secrets_kind": { + "name": "mcp_gateway_config_secrets_kind", + "value": "\"mcp_gateway_config_secrets\".\"secret_kind\" IN ('static_provider_credentials', 'dynamic_registration', 'static_headers')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_configs": { + "name": "mcp_gateway_configs", + "schema": "", + "columns": { + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharing_mode": { + "name": "sharing_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_scope_source": { + "name": "provider_scope_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "provider_resource": { + "name": "provider_resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "path_passthrough": { + "name": "path_passthrough", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "discovered_provider_metadata": { + "name": "discovered_provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "registry_metadata": { + "name": "registry_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "auxiliary_headers": { + "name": "auxiliary_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_configs_owner": { + "name": "IDX_mcp_gateway_configs_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_enabled": { + "name": "IDX_mcp_gateway_configs_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_remote_url": { + "name": "IDX_mcp_gateway_configs_remote_url", + "columns": [ + { + "expression": "remote_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_configs_name_not_empty": { + "name": "mcp_gateway_configs_name_not_empty", + "value": "length(trim(\"mcp_gateway_configs\".\"name\")) > 0" + }, + "mcp_gateway_configs_config_version_positive": { + "name": "mcp_gateway_configs_config_version_positive", + "value": "\"mcp_gateway_configs\".\"config_version\" > 0" + }, + "mcp_gateway_configs_personal_single_user": { + "name": "mcp_gateway_configs_personal_single_user", + "value": "\"mcp_gateway_configs\".\"owner_scope\" <> 'personal' OR \"mcp_gateway_configs\".\"sharing_mode\" = 'single_user'" + }, + "mcp_gateway_configs_owner_scope": { + "name": "mcp_gateway_configs_owner_scope", + "value": "\"mcp_gateway_configs\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_configs_auth_mode": { + "name": "mcp_gateway_configs_auth_mode", + "value": "\"mcp_gateway_configs\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_configs_sharing_mode": { + "name": "mcp_gateway_configs_sharing_mode", + "value": "\"mcp_gateway_configs\".\"sharing_mode\" IN ('single_user', 'multi_user')" + }, + "mcp_gateway_configs_provider_scope_source": { + "name": "mcp_gateway_configs_provider_scope_source", + "value": "\"mcp_gateway_configs\".\"provider_scope_source\" IN ('none', 'discovered', 'override')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connect_resources": { + "name": "mcp_gateway_connect_resources", + "schema": "", + "columns": { + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_status": { + "name": "route_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "route_version": { + "name": "route_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connect_resources_route_key": { + "name": "UQ_mcp_gateway_connect_resources_route_key", + "columns": [ + { + "expression": "route_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_connect_resources_active_config": { + "name": "UQ_mcp_gateway_connect_resources_active_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connect_resources\".\"route_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_config": { + "name": "IDX_mcp_gateway_connect_resources_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_canonical_url": { + "name": "IDX_mcp_gateway_connect_resources_canonical_url", + "columns": [ + { + "expression": "canonical_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connect_resources", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connect_resources_route_key_format": { + "name": "mcp_gateway_connect_resources_route_key_format", + "value": "\"mcp_gateway_connect_resources\".\"route_key\" ~ '^[A-Za-z0-9_-]{32,}$'" + }, + "mcp_gateway_connect_resources_route_version_positive": { + "name": "mcp_gateway_connect_resources_route_version_positive", + "value": "\"mcp_gateway_connect_resources\".\"route_version\" > 0" + }, + "mcp_gateway_connect_resources_owner_scope": { + "name": "mcp_gateway_connect_resources_owner_scope", + "value": "\"mcp_gateway_connect_resources\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connect_resources_route_status": { + "name": "mcp_gateway_connect_resources_route_status", + "value": "\"mcp_gateway_connect_resources\".\"route_status\" IN ('active', 'rotated', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connection_instances": { + "name": "mcp_gateway_connection_instances", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_status": { + "name": "instance_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "instance_version": { + "name": "instance_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connection_instances_non_terminal": { + "name": "UQ_mcp_gateway_connection_instances_non_terminal", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_config": { + "name": "IDX_mcp_gateway_connection_instances_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_user": { + "name": "IDX_mcp_gateway_connection_instances_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connection_instances_version_positive": { + "name": "mcp_gateway_connection_instances_version_positive", + "value": "\"mcp_gateway_connection_instances\".\"instance_version\" > 0" + }, + "mcp_gateway_connection_instances_owner_scope": { + "name": "mcp_gateway_connection_instances_owner_scope", + "value": "\"mcp_gateway_connection_instances\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connection_instances_status": { + "name": "mcp_gateway_connection_instances_status", + "value": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth', 'revoked', 'removed')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_clients": { + "name": "mcp_gateway_oauth_clients", + "schema": "", + "columns": { + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_token_hash": { + "name": "registration_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "declared_scopes": { + "name": "declared_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "registration_access_token_expires_at": { + "name": "registration_access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_clients_client_id": { + "name": "UQ_mcp_gateway_oauth_clients_client_id", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_oauth_clients_registration_token_hash": { + "name": "UQ_mcp_gateway_oauth_clients_registration_token_hash", + "columns": [ + { + "expression": "registration_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_clients_deleted_at": { + "name": "IDX_mcp_gateway_oauth_clients_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_clients_client_id_format": { + "name": "mcp_gateway_oauth_clients_client_id_format", + "value": "\"mcp_gateway_oauth_clients\".\"client_id\" ~ '^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$'" + }, + "mcp_gateway_oauth_clients_auth_method": { + "name": "mcp_gateway_oauth_clients_auth_method", + "value": "\"mcp_gateway_oauth_clients\".\"token_endpoint_auth_method\" IN ('none', 'client_secret_post', 'client_secret_basic')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_grants": { + "name": "mcp_gateway_oauth_grants", + "schema": "", + "columns": { + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_grants_active_binding": { + "name": "UQ_mcp_gateway_oauth_grants_active_binding", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "redirect_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_oauth_grants\".\"revoked_at\" is null and \"mcp_gateway_oauth_grants\".\"grant_status\" in ('pending', 'active')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_client": { + "name": "IDX_mcp_gateway_oauth_grants_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_user": { + "name": "IDX_mcp_gateway_oauth_grants_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_config": { + "name": "IDX_mcp_gateway_oauth_grants_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_owner": { + "name": "IDX_mcp_gateway_oauth_grants_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_resource": { + "name": "IDX_mcp_gateway_oauth_grants_resource", + "columns": [ + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_instance": { + "name": "IDX_mcp_gateway_oauth_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_revoked_at": { + "name": "IDX_mcp_gateway_oauth_grants_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_grants_config_version_positive": { + "name": "mcp_gateway_oauth_grants_config_version_positive", + "value": "\"mcp_gateway_oauth_grants\".\"config_version\" > 0" + }, + "mcp_gateway_oauth_grants_owner_scope": { + "name": "mcp_gateway_oauth_grants_owner_scope", + "value": "\"mcp_gateway_oauth_grants\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_oauth_grants_status": { + "name": "mcp_gateway_oauth_grants_status", + "value": "\"mcp_gateway_oauth_grants\".\"grant_status\" IN ('pending', 'active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_pending_provider_authorizations": { + "name": "mcp_gateway_pending_provider_authorizations", + "schema": "", + "columns": { + "pending_provider_authorization_id": { + "name": "pending_provider_authorization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_authorization_endpoint": { + "name": "provider_authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_token_endpoint": { + "name": "provider_token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pending_status": { + "name": "pending_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_pending_provider_authorizations_state_hash": { + "name": "UQ_mcp_gateway_pending_provider_authorizations_state_hash", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_config": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_grant": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_pending_provider_authorizations\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_expires_at": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_pending_provider_authorizations_config_version_positive": { + "name": "mcp_gateway_pending_provider_authorizations_config_version_positive", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"config_version\" > 0" + }, + "mcp_gateway_pending_provider_authorizations_owner_scope": { + "name": "mcp_gateway_pending_provider_authorizations_owner_scope", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_pending_provider_authorizations_auth_mode": { + "name": "mcp_gateway_pending_provider_authorizations_auth_mode", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_pending_provider_authorizations_status": { + "name": "mcp_gateway_pending_provider_authorizations_status", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"pending_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_provider_grants": { + "name": "mcp_gateway_provider_grants", + "schema": "", + "columns": { + "provider_grant_id": { + "name": "provider_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "encrypted_grant": { + "name": "encrypted_grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject": { + "name": "provider_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_scope": { + "name": "grant_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_provider_grants_active_instance": { + "name": "UQ_mcp_gateway_provider_grants_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_provider_grants\".\"grant_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_provider_grants_instance": { + "name": "IDX_mcp_gateway_provider_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_provider_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_provider_grants_version_positive": { + "name": "mcp_gateway_provider_grants_version_positive", + "value": "\"mcp_gateway_provider_grants\".\"grant_version\" > 0" + }, + "mcp_gateway_provider_grants_status": { + "name": "mcp_gateway_provider_grants_status", + "value": "\"mcp_gateway_provider_grants\".\"grant_status\" IN ('active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_rate_limit_windows": { + "name": "mcp_gateway_rate_limit_windows", + "schema": "", + "columns": { + "rate_limit_window_id": { + "name": "rate_limit_window_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_started_at": { + "name": "window_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_rate_limit_windows_ip_window": { + "name": "UQ_mcp_gateway_rate_limit_windows_ip_window", + "columns": [ + { + "expression": "ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_rate_limit_windows_window": { + "name": "IDX_mcp_gateway_rate_limit_windows_window", + "columns": [ + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_rate_limit_windows_attempt_count_non_negative": { + "name": "mcp_gateway_rate_limit_windows_attempt_count_non_negative", + "value": "\"mcp_gateway_rate_limit_windows\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_refresh_tokens": { + "name": "mcp_gateway_refresh_tokens", + "schema": "", + "columns": { + "refresh_token_id": { + "name": "refresh_token_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotated_from_refresh_token_id": { + "name": "rotated_from_refresh_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_refresh_tokens_token_hash": { + "name": "UQ_mcp_gateway_refresh_tokens_token_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_user": { + "name": "IDX_mcp_gateway_refresh_tokens_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_grant": { + "name": "IDX_mcp_gateway_refresh_tokens_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_refresh_tokens\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_config": { + "name": "IDX_mcp_gateway_refresh_tokens_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_consumed_at": { + "name": "IDX_mcp_gateway_refresh_tokens_consumed_at", + "columns": [ + { + "expression": "consumed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_refresh_tokens_owner_scope": { + "name": "mcp_gateway_refresh_tokens_owner_scope", + "value": "\"mcp_gateway_refresh_tokens\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage": { + "name": "microdollar_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_abuse_classification": { + "name": "idx_abuse_classification", + "columns": [ + { + "expression": "abuse_classification", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id_created_at2": { + "name": "idx_kilo_user_id_created_at2", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_organization_id": { + "name": "idx_microdollar_usage_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily": { + "name": "microdollar_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_microdollar_usage_daily_personal": { + "name": "idx_microdollar_usage_daily_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_daily_org": { + "name": "idx_microdollar_usage_daily_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_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": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_credential_type": { + "name": "provider_credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_resource_id": { + "name": "provider_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_verified_at": { + "name": "provider_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_validated_at": { + "name": "last_validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_access_token_credentials_integration_level": { + "name": "UQ_platform_access_token_credentials_integration_level", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_access_token_credentials_resource": { + "name": "UQ_platform_access_token_credentials_resource", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_credential_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_access_token_credentials_authorized_by_user_id": { + "name": "IDX_platform_access_token_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_access_token_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_platform_access_token_credentials_parent": { + "name": "FK_platform_access_token_credentials_parent", + "tableFrom": "platform_access_token_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_access_token_credentials_credential_version_check": { + "name": "platform_access_token_credentials_credential_version_check", + "value": "\"platform_access_token_credentials\".\"credential_version\" > 0" + }, + "platform_access_token_credentials_resource_id_check": { + "name": "platform_access_token_credentials_resource_id_check", + "value": "\"platform_access_token_credentials\".\"provider_resource_id\" IS NULL OR \"platform_access_token_credentials\".\"provider_resource_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.platform_integrations": { + "name": "platform_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_installation_id": { + "name": "platform_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_id": { + "name": "platform_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_login": { + "name": "platform_account_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "repository_access": { + "name": "repository_access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repositories_synced_at": { + "name": "repositories_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_at": { + "name": "auth_invalid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_reason": { + "name": "auth_invalid_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kilo_requester_user_id": { + "name": "kilo_requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_requester_account_id": { + "name": "platform_requester_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_status": { + "name": "integration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_integrations_owned_by_org_platform_inst": { + "name": "UQ_platform_integrations_owned_by_org_platform_inst", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_owned_by_user_platform_inst": { + "name": "UQ_platform_integrations_owned_by_user_platform_inst", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_slack_platform_inst": { + "name": "UQ_platform_integrations_slack_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'slack' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_linear_platform_inst": { + "name": "UQ_platform_integrations_linear_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'linear' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_user_bitbucket": { + "name": "UQ_platform_integrations_user_bitbucket", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_org_bitbucket": { + "name": "UQ_platform_integrations_org_bitbucket", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_id": { + "name": "IDX_platform_integrations_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_id": { + "name": "IDX_platform_integrations_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_inst_id": { + "name": "IDX_platform_integrations_platform_inst_id", + "columns": [ + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform": { + "name": "IDX_platform_integrations_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_platform": { + "name": "IDX_platform_integrations_owned_by_org_platform", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_platform": { + "name": "IDX_platform_integrations_owned_by_user_platform", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_integration_status": { + "name": "IDX_platform_integrations_integration_status", + "columns": [ + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_kilo_requester": { + "name": "IDX_platform_integrations_kilo_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_requester": { + "name": "IDX_platform_integrations_platform_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_requester_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_integrations_owned_by_organization_id_organizations_id_fk": { + "name": "platform_integrations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_integrations_owned_by_user_id_kilocode_users_id_fk": { + "name": "platform_integrations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_integrations_owner_check": { + "name": "platform_integrations_owner_check", + "value": "(\n (\"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NULL) OR\n (\"platform_integrations\".\"owned_by_user_id\" IS NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.platform_oauth_credentials": { + "name": "platform_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject_login": { + "name": "provider_subject_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_oauth_credentials_platform_integration_id": { + "name": "UQ_platform_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_oauth_credentials_authorized_by_user_id": { + "name": "IDX_platform_oauth_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_oauth_credentials_credential_version_check": { + "name": "platform_oauth_credentials_credential_version_check", + "value": "\"platform_oauth_credentials\".\"credential_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.referral_code_usages": { + "name": "referral_code_usages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "referring_kilo_user_id": { + "name": "referring_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeeming_kilo_user_id": { + "name": "redeeming_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_referral_code_usages_redeeming_kilo_user_id": { + "name": "IDX_referral_code_usages_redeeming_kilo_user_id", + "columns": [ + { + "expression": "redeeming_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_referral_code_usages_redeeming_user_id_code": { + "name": "UQ_referral_code_usages_redeeming_user_id_code", + "nullsNotDistinct": false, + "columns": [ + "redeeming_kilo_user_id", + "referring_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_referral_codes_kilo_user_id": { + "name": "UQ_referral_codes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_referral_codes_code": { + "name": "IDX_referral_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_check_catalog": { + "name": "security_advisor_check_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_check_catalog_check_id_unique": { + "name": "security_advisor_check_catalog_check_id_unique", + "nullsNotDistinct": false, + "columns": [ + "check_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "security_advisor_check_catalog_severity_check": { + "name": "security_advisor_check_catalog_severity_check", + "value": "\"security_advisor_check_catalog\".\"severity\" in ('critical', 'warn', 'info')" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_content": { + "name": "security_advisor_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_content_key_unique": { + "name": "security_advisor_content_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_kiloclaw_coverage": { + "name": "security_advisor_kiloclaw_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_check_ids": { + "name": "match_check_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_kiloclaw_coverage_area_unique": { + "name": "security_advisor_kiloclaw_coverage_area_unique", + "nullsNotDistinct": false, + "columns": [ + "area" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_scans": { + "name": "security_advisor_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_platform": { + "name": "source_platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_method": { + "name": "source_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_ip": { + "name": "public_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "findings_critical": { + "name": "findings_critical", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_warn": { + "name": "findings_warn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_info": { + "name": "findings_info", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_advisor_scans_user_created_at": { + "name": "idx_security_advisor_scans_user_created_at", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_created_at": { + "name": "idx_security_advisor_scans_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_platform": { + "name": "idx_security_advisor_scans_platform", + "columns": [ + { + "expression": "source_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_agent_commands": { + "name": "security_agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_agent_commands_org_created": { + "name": "idx_security_agent_commands_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_user_created": { + "name": "idx_security_agent_commands_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_status_updated": { + "name": "idx_security_agent_commands_status_updated", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_finding_created": { + "name": "idx_security_agent_commands_finding_created", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_commands_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_commands_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_commands_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_finding_id_security_findings_id_fk": { + "name": "security_agent_commands_finding_id_security_findings_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_commands_owner_check": { + "name": "security_agent_commands_owner_check", + "value": "(\n (\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_commands\".\"owned_by_user_id\" IS NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_agent_commands_type_check": { + "name": "security_agent_commands_type_check", + "value": "\"security_agent_commands\".\"command_type\" IN ('sync', 'dismiss_finding', 'start_analysis', 'apply_auto_remediation')" + }, + "security_agent_commands_origin_check": { + "name": "security_agent_commands_origin_check", + "value": "\"security_agent_commands\".\"origin\" IN ('manual', 'dashboard_refresh', 'enable_initial_sync', 'settings_include_existing')" + }, + "security_agent_commands_status_check": { + "name": "security_agent_commands_status_check", + "value": "\"security_agent_commands\".\"status\" IN ('accepted', 'running', 'succeeded', 'failed', 'no_op')" + } + }, + "isRLSEnabled": false + }, + "public.security_agent_repository_sync_state": { + "name": "security_agent_repository_sync_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_succeeded_at": { + "name": "last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_agent_repository_sync_state_org_repo": { + "name": "UQ_security_agent_repository_sync_state_org_repo", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_repository_sync_state_user_repo": { + "name": "UQ_security_agent_repository_sync_state_user_repo", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_repository_sync_state_owner_check": { + "name": "security_agent_repository_sync_state_owner_check", + "value": "(\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_owner_state": { + "name": "security_analysis_owner_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_analysis_enabled_at": { + "name": "auto_analysis_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "block_reason": { + "name": "block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_actor_resolution_failures": { + "name": "consecutive_actor_resolution_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_actor_resolution_failure_at": { + "name": "last_actor_resolution_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_owner_state_org_owner": { + "name": "UQ_security_analysis_owner_state_org_owner", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_analysis_owner_state_user_owner": { + "name": "UQ_security_analysis_owner_state_user_owner", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_owner_state_owner_check": { + "name": "security_analysis_owner_state_owner_check", + "value": "(\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_owner_state_block_reason_check": { + "name": "security_analysis_owner_state_block_reason_check", + "value": "\"security_analysis_owner_state\".\"block_reason\" IS NULL OR \"security_analysis_owner_state\".\"block_reason\" IN ('INSUFFICIENT_CREDITS', 'ACTOR_RESOLUTION_FAILED', 'OPERATOR_PAUSE')" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_queue": { + "name": "security_analysis_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queue_status": { + "name": "queue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity_rank": { + "name": "severity_rank", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reopen_requeue_count": { + "name": "reopen_requeue_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_queue_finding_id": { + "name": "UQ_security_analysis_queue_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_org": { + "name": "idx_security_analysis_queue_claim_path_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_user": { + "name": "idx_security_analysis_queue_claim_path_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_org": { + "name": "idx_security_analysis_queue_in_flight_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_user": { + "name": "idx_security_analysis_queue_in_flight_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_lag_dashboards": { + "name": "idx_security_analysis_queue_lag_dashboards", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_pending_reconciliation": { + "name": "idx_security_analysis_queue_pending_reconciliation", + "columns": [ + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_running_reconciliation": { + "name": "idx_security_analysis_queue_running_reconciliation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_failure_trend": { + "name": "idx_security_analysis_queue_failure_trend", + "columns": [ + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"failure_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_queue_finding_id_security_findings_id_fk": { + "name": "security_analysis_queue_finding_id_security_findings_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_queue_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_queue_owner_check": { + "name": "security_analysis_queue_owner_check", + "value": "(\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_queue_status_check": { + "name": "security_analysis_queue_status_check", + "value": "\"security_analysis_queue\".\"queue_status\" IN ('queued', 'pending', 'running', 'failed', 'completed')" + }, + "security_analysis_queue_claim_token_required_check": { + "name": "security_analysis_queue_claim_token_required_check", + "value": "\"security_analysis_queue\".\"queue_status\" NOT IN ('pending', 'running') OR \"security_analysis_queue\".\"claim_token\" IS NOT NULL" + }, + "security_analysis_queue_attempt_count_non_negative_check": { + "name": "security_analysis_queue_attempt_count_non_negative_check", + "value": "\"security_analysis_queue\".\"attempt_count\" >= 0" + }, + "security_analysis_queue_reopen_requeue_count_non_negative_check": { + "name": "security_analysis_queue_reopen_requeue_count_non_negative_check", + "value": "\"security_analysis_queue\".\"reopen_requeue_count\" >= 0" + }, + "security_analysis_queue_severity_rank_check": { + "name": "security_analysis_queue_severity_rank_check", + "value": "\"security_analysis_queue\".\"severity_rank\" IN (0, 1, 2, 3)" + }, + "security_analysis_queue_failure_code_check": { + "name": "security_analysis_queue_failure_code_check", + "value": "\"security_analysis_queue\".\"failure_code\" IS NULL OR \"security_analysis_queue\".\"failure_code\" IN (\n 'NETWORK_TIMEOUT',\n 'UPSTREAM_5XX',\n 'TEMP_TOKEN_FAILURE',\n 'START_CALL_AMBIGUOUS',\n 'REQUEUE_TEMPORARY_PRECONDITION',\n 'ACTOR_RESOLUTION_FAILED',\n 'GITHUB_TOKEN_UNAVAILABLE',\n 'INVALID_CONFIG',\n 'MISSING_OWNERSHIP',\n 'PERMISSION_DENIED_PERMANENT',\n 'UNSUPPORTED_SEVERITY',\n 'INSUFFICIENT_CREDITS',\n 'STATE_GUARD_REJECTED',\n 'SKIPPED_ALREADY_IN_PROGRESS',\n 'SKIPPED_NO_LONGER_ELIGIBLE',\n 'REOPEN_LOOP_GUARD',\n 'RUN_LOST'\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_audit_log": { + "name": "security_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source_occurred_at": { + "name": "source_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "finding_snapshot": { + "name": "finding_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_security_audit_log_org_created": { + "name": "IDX_security_audit_log_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_created": { + "name": "IDX_security_audit_log_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_resource": { + "name": "IDX_security_audit_log_resource", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_actor": { + "name": "IDX_security_audit_log_actor", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_action": { + "name": "IDX_security_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_org_event_key": { + "name": "UQ_security_audit_log_org_event_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_user_event_key": { + "name": "UQ_security_audit_log_user_event_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_org_occurred": { + "name": "IDX_security_audit_log_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_occurred": { + "name": "IDX_security_audit_log_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_audit_log_owned_by_organization_id_organizations_id_fk": { + "name": "security_audit_log_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_audit_log_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_audit_log_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_audit_log_owner_check": { + "name": "security_audit_log_owner_check", + "value": "(\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NULL) OR (\"security_audit_log\".\"owned_by_user_id\" IS NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "security_audit_log_action_check": { + "name": "security_audit_log_action_check", + "value": "\"security_audit_log\".\"action\" IN ('security.finding.created', 'security.finding.severity_changed', 'security.finding.status_change', 'security.finding.dismissed', 'security.finding.auto_dismissed', 'security.finding.superseded', 'security.finding.analysis_started', 'security.finding.analysis_completed', 'security.finding.analysis_failed', 'security.remediation.queued', 'security.remediation.started', 'security.remediation.pr_opened', 'security.remediation.failed', 'security.remediation.blocked', 'security.remediation.no_changes_needed', 'security.remediation.cancelled', 'security.remediation.retried', 'security.finding.deleted', 'security.config.enabled', 'security.config.disabled', 'security.config.updated', 'security.sync.triggered', 'security.sync.completed', 'security.audit_log.exported', 'security.audit_report.generated')" + }, + "security_audit_log_actor_type_check": { + "name": "security_audit_log_actor_type_check", + "value": "\"security_audit_log\".\"actor_type\" IN ('customer_user', 'kilo_admin', 'system')" + }, + "security_audit_log_source_context_check": { + "name": "security_audit_log_source_context_check", + "value": "\"security_audit_log\".\"source_context\" IN ('security_sync', 'web', 'analysis_worker', 'remediation_callback', 'rollout_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.security_finding_notifications": { + "name": "security_finding_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'staged'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_finding_notifications_finding_recipient_kind": { + "name": "uq_security_finding_notifications_finding_recipient_kind", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_pending": { + "name": "idx_security_finding_notifications_pending", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_staged": { + "name": "idx_security_finding_notifications_staged", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'staged'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_finding_id": { + "name": "idx_security_finding_notifications_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_recipient_user_id": { + "name": "idx_security_finding_notifications_recipient_user_id", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_finding_notifications_finding_fk": { + "name": "security_finding_notifications_finding_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_finding_notifications_recipient_fk": { + "name": "security_finding_notifications_recipient_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_finding_notifications_kind_check": { + "name": "security_finding_notifications_kind_check", + "value": "\"security_finding_notifications\".\"kind\" IN ('new_finding', 'sla_warning', 'sla_breach')" + }, + "security_finding_notifications_status_check": { + "name": "security_finding_notifications_status_check", + "value": "\"security_finding_notifications\".\"status\" IN ('staged', 'pending', 'sending', 'sent', 'failed', 'cancelled')" + }, + "security_finding_notifications_attempt_count_check": { + "name": "security_finding_notifications_attempt_count_check", + "value": "\"security_finding_notifications\".\"attempt_count\" >= 0" + }, + "security_finding_notifications_claimed_at_check": { + "name": "security_finding_notifications_claimed_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NULL)\n )" + }, + "security_finding_notifications_sent_at_check": { + "name": "security_finding_notifications_sent_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NULL)\n )" + }, + "security_finding_notifications_error_message_length_check": { + "name": "security_finding_notifications_error_message_length_check", + "value": "\"security_finding_notifications\".\"error_message\" IS NULL OR length(\"security_finding_notifications\".\"error_message\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.security_findings": { + "name": "security_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ghsa_id": { + "name": "ghsa_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cve_id": { + "name": "cve_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_ecosystem": { + "name": "package_ecosystem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vulnerable_version_range": { + "name": "vulnerable_version_range", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patched_version": { + "name": "patched_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_path": { + "name": "manifest_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "ignored_reason": { + "name": "ignored_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignored_by": { + "name": "ignored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_at": { + "name": "fixed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sla_due_at": { + "name": "sla_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dependabot_html_url": { + "name": "dependabot_html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwe_ids": { + "name": "cwe_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cvss_score": { + "name": "cvss_score", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": false + }, + "dependency_scope": { + "name": "dependency_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_status": { + "name": "analysis_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_started_at": { + "name": "analysis_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_error": { + "name": "analysis_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis": { + "name": "analysis", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_detected_at": { + "name": "first_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_findings_user_source": { + "name": "uq_security_findings_user_source", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_security_findings_org_source": { + "name": "uq_security_findings_org_source", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_id": { + "name": "idx_security_findings_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_id": { + "name": "idx_security_findings_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_repo": { + "name": "idx_security_findings_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_severity": { + "name": "idx_security_findings_severity", + "columns": [ + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_status": { + "name": "idx_security_findings_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_package": { + "name": "idx_security_findings_package", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_sla_due_at": { + "name": "idx_security_findings_sla_due_at", + "columns": [ + { + "expression": "sla_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_session_id": { + "name": "idx_security_findings_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_cli_session_id": { + "name": "idx_security_findings_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_analysis_status": { + "name": "idx_security_findings_analysis_status", + "columns": [ + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_analysis_in_flight": { + "name": "idx_security_findings_org_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_analysis_in_flight": { + "name": "idx_security_findings_user_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_findings_owned_by_organization_id_organizations_id_fk": { + "name": "security_findings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_findings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_findings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_findings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_platform_integration_id_platform_integrations_id_fk": { + "name": "security_findings_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "security_findings", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_findings_owner_check": { + "name": "security_findings_owner_check", + "value": "(\n (\"security_findings\".\"owned_by_user_id\" IS NOT NULL AND \"security_findings\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_findings\".\"owned_by_user_id\" IS NULL AND \"security_findings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_remediation_attempts": { + "name": "security_remediation_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "remediation_id": { + "name": "remediation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_fingerprint": { + "name": "analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "remediation_model_slug": { + "name": "remediation_model_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_attempt_count": { + "name": "launch_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "callback_attempt_token_hash": { + "name": "callback_attempt_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_result": { + "name": "structured_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_assistant_message": { + "name": "final_assistant_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validation_evidence": { + "name": "validation_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "risk_notes": { + "name": "risk_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_reason": { + "name": "draft_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_by_user_id": { + "name": "cancellation_requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "launched_at": { + "name": "launched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediation_attempts_number": { + "name": "UQ_security_remediation_attempts_number", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_finding": { + "name": "UQ_security_remediation_attempts_active_finding", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_remediation": { + "name": "UQ_security_remediation_attempts_active_remediation", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_finding_fingerprint_terminal": { + "name": "UQ_security_remediation_attempts_finding_fingerprint_terminal", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_claim": { + "name": "idx_security_remediation_attempts_org_claim", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_claim": { + "name": "idx_security_remediation_attempts_user_claim", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_claim": { + "name": "idx_security_remediation_attempts_repo_claim", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_inflight": { + "name": "idx_security_remediation_attempts_org_inflight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_inflight": { + "name": "idx_security_remediation_attempts_user_inflight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_inflight": { + "name": "idx_security_remediation_attempts_repo_inflight", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_cloud_agent_session": { + "name": "idx_security_remediation_attempts_cloud_agent_session", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_finding_fingerprint": { + "name": "idx_security_remediation_attempts_finding_fingerprint", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediation_attempts_remediation_id_security_remediations_id_fk": { + "name": "security_remediation_attempts_remediation_id_security_remediations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_remediations", + "columnsFrom": [ + "remediation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_finding_id_security_findings_id_fk": { + "name": "security_remediation_attempts_finding_id_security_findings_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediation_attempts_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "cancellation_requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediation_attempts_owner_check": { + "name": "security_remediation_attempts_owner_check", + "value": "(\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediation_attempts_status_check": { + "name": "security_remediation_attempts_status_check", + "value": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + }, + "security_remediation_attempts_origin_check": { + "name": "security_remediation_attempts_origin_check", + "value": "\"security_remediation_attempts\".\"origin\" IN ('auto_policy', 'bulk_existing', 'manual')" + }, + "security_remediation_attempts_attempt_number_check": { + "name": "security_remediation_attempts_attempt_number_check", + "value": "\"security_remediation_attempts\".\"attempt_number\" >= 1" + }, + "security_remediation_attempts_launch_attempt_count_check": { + "name": "security_remediation_attempts_launch_attempt_count_check", + "value": "\"security_remediation_attempts\".\"launch_attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.security_remediations": { + "name": "security_remediations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "latest_attempt_id": { + "name": "latest_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_fingerprint": { + "name": "latest_analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_completed_at": { + "name": "latest_analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediations_finding_id": { + "name": "UQ_security_remediations_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_org_status": { + "name": "idx_security_remediations_org_status", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_user_status": { + "name": "idx_security_remediations_user_status", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_repo_status": { + "name": "idx_security_remediations_repo_status", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_latest_attempt": { + "name": "idx_security_remediations_latest_attempt", + "columns": [ + { + "expression": "latest_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediations_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_finding_id_security_findings_id_fk": { + "name": "security_remediations_finding_id_security_findings_id_fk", + "tableFrom": "security_remediations", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediations_owner_check": { + "name": "security_remediations_owner_check", + "value": "(\n (\"security_remediations\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediations\".\"owned_by_user_id\" IS NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediations_status_check": { + "name": "security_remediations_status_check", + "value": "\"security_remediations\".\"status\" IN ('queued', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.shared_cli_sessions": { + "name": "shared_cli_sessions", + "schema": "", + "columns": { + "share_id": { + "name": "share_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_state": { + "name": "shared_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_shared_cli_sessions_session_id": { + "name": "IDX_shared_cli_sessions_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_shared_cli_sessions_created_at": { + "name": "IDX_shared_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_cli_sessions_session_id_cli_sessions_session_id_fk": { + "name": "shared_cli_sessions_session_id_cli_sessions_session_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_cli_sessions_shared_state_check": { + "name": "shared_cli_sessions_shared_state_check", + "value": "\"shared_cli_sessions\".\"shared_state\" IN ('public', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.slack_bot_requests": { + "name": "slack_bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_name": { + "name": "slack_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message_truncated": { + "name": "user_message_truncated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_calls_made": { + "name": "tool_calls_made", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_slack_bot_requests_created_at": { + "name": "idx_slack_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_slack_team_id": { + "name": "idx_slack_bot_requests_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_org_id": { + "name": "idx_slack_bot_requests_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_user_id": { + "name": "idx_slack_bot_requests_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_status": { + "name": "idx_slack_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_event_type": { + "name": "idx_slack_bot_requests_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_team_created": { + "name": "idx_slack_bot_requests_team_created", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_bot_requests_owned_by_organization_id_organizations_id_fk": { + "name": "slack_bot_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_bot_requests_owner_check": { + "name": "slack_bot_requests_owner_check", + "value": "(\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NOT NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NOT NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.source_embeddings": { + "name": "source_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_base_branch": { + "name": "is_base_branch", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_source_embeddings_organization_id": { + "name": "IDX_source_embeddings_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_kilo_user_id": { + "name": "IDX_source_embeddings_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_project_id": { + "name": "IDX_source_embeddings_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_created_at": { + "name": "IDX_source_embeddings_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_updated_at": { + "name": "IDX_source_embeddings_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_file_path_lower": { + "name": "IDX_source_embeddings_file_path_lower", + "columns": [ + { + "expression": "LOWER(\"file_path\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_git_branch": { + "name": "IDX_source_embeddings_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_org_project_branch": { + "name": "IDX_source_embeddings_org_project_branch", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_embeddings_organization_id_organizations_id_fk": { + "name": "source_embeddings_organization_id_organizations_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "source_embeddings_kilo_user_id_kilocode_users_id_fk": { + "name": "source_embeddings_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_source_embeddings_org_project_branch_file_lines": { + "name": "UQ_source_embeddings_org_project_branch_file_lines", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "project_id", + "git_branch", + "file_path", + "start_line", + "end_line" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stripe_dispute_actions": { + "name": "stripe_dispute_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_actions_case_id": { + "name": "IDX_stripe_dispute_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_actions_claim_path": { + "name": "IDX_stripe_dispute_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk": { + "name": "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk", + "tableFrom": "stripe_dispute_actions", + "tableTo": "stripe_dispute_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_actions_case_type_target": { + "name": "UQ_stripe_dispute_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_actions_action_type_check": { + "name": "stripe_dispute_actions_action_type_check", + "value": "\"stripe_dispute_actions\".\"action_type\" IN ('stripe_acceptance', 'user_block', 'auto_top_up_disable', 'credit_balance_reset', 'subscription_cancellation', 'access_termination', 'kiloclaw_suspension')" + }, + "stripe_dispute_actions_status_check": { + "name": "stripe_dispute_actions_status_check", + "value": "\"stripe_dispute_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'skipped')" + }, + "stripe_dispute_actions_attempt_count_non_negative_check": { + "name": "stripe_dispute_actions_attempt_count_non_negative_check", + "value": "\"stripe_dispute_actions\".\"attempt_count\" >= 0" + }, + "stripe_dispute_actions_target_key_not_empty_check": { + "name": "stripe_dispute_actions_target_key_not_empty_check", + "value": "length(\"stripe_dispute_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_dispute_cases": { + "name": "stripe_dispute_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_dispute_id": { + "name": "stripe_dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_created_at": { + "name": "stripe_event_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispute_reason": { + "name": "dispute_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_status": { + "name": "stripe_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'needs_action'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_created_at": { + "name": "stripe_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evidence_due_by": { + "name": "evidence_due_by", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by_kilo_user_id": { + "name": "accepted_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance_started_at": { + "name": "acceptance_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enforcement_completed_at": { + "name": "enforcement_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_cases_event_id": { + "name": "IDX_stripe_dispute_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_charge_id": { + "name": "IDX_stripe_dispute_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_payment_intent_id": { + "name": "IDX_stripe_dispute_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_customer_id": { + "name": "IDX_stripe_dispute_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_kilo_user_id": { + "name": "IDX_stripe_dispute_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_organization_id": { + "name": "IDX_stripe_dispute_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_status_due_by": { + "name": "IDX_stripe_dispute_cases_status_due_by", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "evidence_due_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_organization_id_organizations_id_fk": { + "name": "stripe_dispute_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "accepted_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_cases_dispute_id": { + "name": "UQ_stripe_dispute_cases_dispute_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_dispute_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_cases_owner_classification_check": { + "name": "stripe_dispute_cases_owner_classification_check", + "value": "\"stripe_dispute_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_dispute_cases_status_check": { + "name": "stripe_dispute_cases_status_check", + "value": "\"stripe_dispute_cases\".\"status\" IN ('needs_action', 'processing', 'accepted', 'acceptance_failed', 'enforcement_failed', 'review_required', 'closed')" + }, + "stripe_dispute_cases_amount_minor_units_non_negative_check": { + "name": "stripe_dispute_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_dispute_cases\".\"amount_minor_units\" IS NULL OR \"stripe_dispute_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_actions": { + "name": "stripe_early_fraud_warning_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_actions_case_id": { + "name": "IDX_stripe_early_fraud_warning_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_actions_claim_path": { + "name": "IDX_stripe_early_fraud_warning_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk": { + "name": "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk", + "tableFrom": "stripe_early_fraud_warning_actions", + "tableTo": "stripe_early_fraud_warning_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_actions_case_type_target": { + "name": "UQ_stripe_early_fraud_warning_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_actions_action_type_check": { + "name": "stripe_early_fraud_warning_actions_action_type_check", + "value": "\"stripe_early_fraud_warning_actions\".\"action_type\" IN ('containment', 'refund', 'payment_value_clawback', 'subscription_termination', 'access_termination', 'kiloclaw_suspension', 'affiliate_payout_reversal', 'referral_reward_reversal', 'user_notice')" + }, + "stripe_early_fraud_warning_actions_status_check": { + "name": "stripe_early_fraud_warning_actions_status_check", + "value": "\"stripe_early_fraud_warning_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'review_required', 'dismissed')" + }, + "stripe_early_fraud_warning_actions_attempt_count_non_negative_check": { + "name": "stripe_early_fraud_warning_actions_attempt_count_non_negative_check", + "value": "\"stripe_early_fraud_warning_actions\".\"attempt_count\" >= 0" + }, + "stripe_early_fraud_warning_actions_target_key_not_empty_check": { + "name": "stripe_early_fraud_warning_actions_target_key_not_empty_check", + "value": "length(\"stripe_early_fraud_warning_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_cases": { + "name": "stripe_early_fraud_warning_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_early_fraud_warning_id": { + "name": "stripe_early_fraud_warning_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warning_created_at": { + "name": "warning_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contained_at": { + "name": "contained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remediated_at": { + "name": "remediated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_cases_event_id": { + "name": "IDX_stripe_early_fraud_warning_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_charge_id": { + "name": "IDX_stripe_early_fraud_warning_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_payment_intent_id": { + "name": "IDX_stripe_early_fraud_warning_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_customer_id": { + "name": "IDX_stripe_early_fraud_warning_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_kilo_user_id": { + "name": "IDX_stripe_early_fraud_warning_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_organization_id": { + "name": "IDX_stripe_early_fraud_warning_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_status_created_at": { + "name": "IDX_stripe_early_fraud_warning_cases_status_created_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk": { + "name": "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_cases_warning_id": { + "name": "UQ_stripe_early_fraud_warning_cases_warning_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_early_fraud_warning_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_cases_owner_classification_check": { + "name": "stripe_early_fraud_warning_cases_owner_classification_check", + "value": "\"stripe_early_fraud_warning_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_early_fraud_warning_cases_status_check": { + "name": "stripe_early_fraud_warning_cases_status_check", + "value": "\"stripe_early_fraud_warning_cases\".\"status\" IN ('queued', 'contained', 'processing', 'completed', 'review_required', 'failed', 'remediated', 'dismissed')" + }, + "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check": { + "name": "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_early_fraud_warning_cases\".\"amount_minor_units\" IS NULL OR \"stripe_early_fraud_warning_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stytch_fingerprints": { + "name": "stytch_fingerprints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_fingerprint": { + "name": "visitor_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_fingerprint": { + "name": "browser_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_id": { + "name": "browser_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hardware_fingerprint": { + "name": "hardware_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_fingerprint": { + "name": "network_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_id": { + "name": "visitor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict_action": { + "name": "verdict_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_device_type": { + "name": "detected_device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_authentic_device": { + "name": "is_authentic_device", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"\"}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint_data": { + "name": "fingerprint_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_free_tier_allowed": { + "name": "kilo_free_tier_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_hardware_fingerprint": { + "name": "idx_hardware_fingerprint", + "columns": [ + { + "expression": "hardware_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id": { + "name": "idx_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_stytch_fingerprints_reasons_gin": { + "name": "idx_stytch_fingerprints_reasons_gin", + "columns": [ + { + "expression": "reasons", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_verdict_action": { + "name": "idx_verdict_action", + "columns": [ + { + "expression": "verdict_action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_visitor_fingerprint": { + "name": "idx_visitor_fingerprint", + "columns": [ + { + "expression": "visitor_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_prompt_prefix": { + "name": "system_prompt_prefix", + "schema": "", + "columns": { + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_system_prompt_prefix": { + "name": "UQ_system_prompt_prefix", + "columns": [ + { + "expression": "system_prompt_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactional_email_log": { + "name": "transactional_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_transactional_email_log_type_idempotency_key": { + "name": "UQ_transactional_email_log_type_idempotency_key", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_user_id": { + "name": "IDX_transactional_email_log_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_organization_id": { + "name": "IDX_transactional_email_log_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_email_log_user_id_kilocode_users_id_fk": { + "name": "transactional_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactional_email_log_organization_id_organizations_id_fk": { + "name": "transactional_email_log_organization_id_organizations_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_transactional_email_log_owner": { + "name": "CHK_transactional_email_log_owner", + "value": "\"transactional_email_log\".\"user_id\" IS NOT NULL OR \"transactional_email_log\".\"organization_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.user_admin_notes": { + "name": "user_admin_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note_content": { + "name": "note_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_kilo_user_id": { + "name": "admin_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_34517df0b385234babc38fe81b": { + "name": "IDX_34517df0b385234babc38fe81b", + "columns": [ + { + "expression": "admin_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_ccbde98c4c14046daa5682ec4f": { + "name": "IDX_ccbde98c4c14046daa5682ec4f", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_d0270eb24ef6442d65a0b7853c": { + "name": "IDX_d0270eb24ef6442d65a0b7853c", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_affiliate_attributions": { + "name": "user_affiliate_attributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_id": { + "name": "tracking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_attributions_user_id": { + "name": "IDX_user_affiliate_attributions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_attributions_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_attributions_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_attributions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_attributions_user_provider": { + "name": "UQ_user_affiliate_attributions_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_attributions_provider_check": { + "name": "user_affiliate_attributions_provider_check", + "value": "\"user_affiliate_attributions\".\"provider\" IN ('impact')" + } + }, + "isRLSEnabled": false + }, + "public.user_affiliate_events": { + "name": "user_affiliate_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_event_id": { + "name": "parent_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_action_id": { + "name": "impact_action_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_submission_uri": { + "name": "impact_submission_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_events_claim_path": { + "name": "IDX_user_affiliate_events_claim_path", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_parent_event_id": { + "name": "IDX_user_affiliate_events_parent_event_id", + "columns": [ + { + "expression": "parent_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_provider_event_type_charge": { + "name": "IDX_user_affiliate_events_provider_event_type_charge", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_events_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_events_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_affiliate_events_parent_event_id_fk": { + "name": "user_affiliate_events_parent_event_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "user_affiliate_events", + "columnsFrom": [ + "parent_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_events_dedupe_key": { + "name": "UQ_user_affiliate_events_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_events_provider_check": { + "name": "user_affiliate_events_provider_check", + "value": "\"user_affiliate_events\".\"provider\" IN ('impact')" + }, + "user_affiliate_events_event_type_check": { + "name": "user_affiliate_events_event_type_check", + "value": "\"user_affiliate_events\".\"event_type\" IN ('signup', 'trial_start', 'trial_end', 'sale', 'sale_reversal')" + }, + "user_affiliate_events_delivery_state_check": { + "name": "user_affiliate_events_delivery_state_check", + "value": "\"user_affiliate_events\".\"delivery_state\" IN ('queued', 'blocked', 'sending', 'delivered', 'failed')" + }, + "user_affiliate_events_attempt_count_non_negative_check": { + "name": "user_affiliate_events_attempt_count_non_negative_check", + "value": "\"user_affiliate_events\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_auth_provider": { + "name": "user_auth_provider", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_auth_provider_kilo_user_id": { + "name": "IDX_user_auth_provider_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_hosted_domain": { + "name": "IDX_user_auth_provider_hosted_domain", + "columns": [ + { + "expression": "hosted_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_auth_provider_provider_provider_account_id_pk": { + "name": "user_auth_provider_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_feedback": { + "name": "user_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feedback_for": { + "name": "feedback_for", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "feedback_batch": { + "name": "feedback_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_feedback_created_at": { + "name": "IDX_user_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_kilo_user_id": { + "name": "IDX_user_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_for": { + "name": "IDX_user_feedback_feedback_for", + "columns": [ + { + "expression": "feedback_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_batch": { + "name": "IDX_user_feedback_feedback_batch", + "columns": [ + { + "expression": "feedback_batch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_source": { + "name": "IDX_user_feedback_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "user_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_github_app_tokens": { + "name": "user_github_app_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_github_app_tokens_user_app": { + "name": "UQ_user_github_app_tokens_user_app", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_github_app_tokens_github_user_app": { + "name": "UQ_user_github_app_tokens_github_user_app", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_github_app_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_github_app_tokens_app_type_check": { + "name": "user_github_app_tokens_app_type_check", + "value": "\"user_github_app_tokens\".\"github_app_type\" IN ('standard', 'lite')" + } + }, + "isRLSEnabled": false + }, + "public.user_model_preferences": { + "name": "user_model_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favorites": { + "name": "favorites", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_selected": { + "name": "last_selected", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_model_preferences_user_id": { + "name": "UQ_user_model_preferences_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_model_preferences_user_id_kilocode_users_id_fk": { + "name": "user_model_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_model_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_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 648d354d66..417f3de33d 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1303,6 +1303,13 @@ "when": 1784129617576, "tag": "0185_lowly_mandroid", "breakpoints": true + }, + { + "idx": 186, + "version": "7", + "when": 1784199969392, + "tag": "0186_milky_amazoness", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.test.ts b/packages/db/src/schema.test.ts index bd45cafe7b..021740bd35 100644 --- a/packages/db/src/schema.test.ts +++ b/packages/db/src/schema.test.ts @@ -233,6 +233,7 @@ async function insertCodingPlanSubscription(values: { } type PlatformIntegrationInsert = typeof schema.platform_integrations.$inferInsert; +type PlatformOAuthCredentialInsert = typeof schema.platform_oauth_credentials.$inferInsert; type PlatformAccessTokenCredentialInsert = typeof schema.platform_access_token_credentials.$inferInsert; @@ -330,6 +331,32 @@ async function insertPlatformAccessTokenCredential( return credential; } +async function insertPlatformOAuthCredential( + integration: typeof schema.platform_integrations.$inferSelect, + overrides: Partial = {} +): Promise { + const [credential] = await schemaTestDb.db + .insert(schema.platform_oauth_credentials) + .values({ + platform_integration_id: integration.id, + authorized_by_user_id: null, + provider_subject_id: 'gitlab-subject-123', + provider_subject_login: 'gitlab-user', + provider_base_url: 'https://gitlab.example.com', + access_token_encrypted: 'encrypted-gitlab-oauth-access-token', + refresh_token_encrypted: null, + oauth_client_secret_encrypted: 'encrypted-gitlab-oauth-client-secret', + ...overrides, + }) + .returning(); + + if (!credential) { + throw new Error('Failed to insert platform OAuth credential'); + } + + return credential; +} + async function expectPlatformCredentialConstraintViolation( insertPromise: Promise, constraint: string @@ -781,7 +808,112 @@ describe('database schema', () => { expect(Object.hasOwn(schema, 'platform_access_token_credentials')).toBe(true); }); + describe('platform OAuth credentials', () => { + it('stores a legacy GitLab OAuth credential with a custom client secret and no recoverable authorizer or refresh token', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + platform: 'gitlab', + integration_type: 'oauth', + }); + + const credential = await insertPlatformOAuthCredential(integration); + + expect(credential).toEqual( + expect.objectContaining({ + platform_integration_id: integration.id, + authorized_by_user_id: null, + provider_base_url: 'https://gitlab.example.com', + refresh_token_encrypted: null, + oauth_client_secret_encrypted: 'encrypted-gitlab-oauth-client-secret', + }) + ); + }); + }); + + it('rejects a non-positive credential version', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + platform: 'gitlab', + integration_type: 'oauth', + }); + + await expectPlatformCredentialConstraintViolation( + insertPlatformOAuthCredential(integration, { credential_version: 0 }), + 'platform_oauth_credentials_credential_version_check' + ); + }); + }); + }); + describe('platform access token credentials', () => { + it('stores a GitLab PAT credential without fabricated organization or validation evidence', async () => { + await withPlatformAccessTokenTestData(async ({ userId, organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + owned_by_organization_id: null, + owned_by_user_id: userId, + platform: 'gitlab', + integration_type: 'pat', + }); + + const credential = await insertPlatformAccessTokenCredential(integration, { + provider_credential_type: 'personal_access_token', + provider_scopes: null, + provider_verified_at: null, + last_validated_at: null, + provider_resource_id: null, + provider_base_url: 'https://gitlab.example.com', + authorized_by_user_id: userId, + provider_metadata: {}, + }); + + expect(credential).toEqual( + expect.objectContaining({ + provider_credential_type: 'personal_access_token', + provider_scopes: null, + provider_verified_at: null, + last_validated_at: null, + provider_resource_id: null, + provider_base_url: 'https://gitlab.example.com', + authorized_by_user_id: userId, + provider_metadata: {}, + }) + ); + }); + }); + + it('stores one integration-level credential and multiple resource-scoped credentials', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + platform: 'gitlab', + integration_type: 'pat', + }); + + await insertPlatformAccessTokenCredential(integration, { + provider_credential_type: 'personal_access_token', + provider_base_url: 'https://gitlab.example.com', + }); + await insertPlatformAccessTokenCredential(integration, { + provider_credential_type: 'project_access_token', + provider_resource_id: '100', + provider_base_url: 'https://gitlab.example.com', + }); + await insertPlatformAccessTokenCredential(integration, { + provider_credential_type: 'project_access_token', + provider_resource_id: '200', + provider_base_url: 'https://gitlab.example.com', + }); + + expect( + await schemaTestDb.db + .select() + .from(schema.platform_access_token_credentials) + .where( + eq(schema.platform_access_token_credentials.platform_integration_id, integration.id) + ) + ).toHaveLength(3); + }); + }); + it('stores one verified Bitbucket Workspace Access Token credential whose parent owns its identity', async () => { await withPlatformAccessTokenTestData(async ({ organizationId }) => { const integration = await insertPlatformIntegration(organizationId); @@ -791,9 +923,6 @@ describe('database schema', () => { expect(credential).toEqual( expect.objectContaining({ platform_integration_id: integration.id, - owned_by_organization_id: null, - platform: null, - integration_type: null, provider_credential_type: 'workspace_access_token', credential_version: 1, }) @@ -822,14 +951,61 @@ describe('database schema', () => { }); }); - it('rejects a second credential for the same integration', async () => { + it('rejects a non-positive credential version', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId); + + await expectPlatformCredentialConstraintViolation( + insertPlatformAccessTokenCredential(integration, { credential_version: 0 }), + 'platform_access_token_credentials_credential_version_check' + ); + }); + }); + + it('rejects an empty resource ID', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + platform: 'gitlab', + integration_type: 'pat', + }); + + await expectPlatformCredentialConstraintViolation( + insertPlatformAccessTokenCredential(integration, { + provider_credential_type: 'project_access_token', + provider_resource_id: '', + }), + 'platform_access_token_credentials_resource_id_check' + ); + }); + }); + + it('rejects a second integration-level 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' + 'UQ_platform_access_token_credentials_integration_level' + ); + }); + }); + + it('rejects a duplicate resource credential for the same integration and type', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + platform: 'gitlab', + integration_type: 'pat', + }); + const resourceCredential: Partial = { + provider_credential_type: 'project_access_token', + provider_resource_id: '100', + }; + await insertPlatformAccessTokenCredential(integration, resourceCredential); + + await expectPlatformCredentialConstraintViolation( + insertPlatformAccessTokenCredential(integration, resourceCredential), + 'UQ_platform_access_token_credentials_resource' ); }); }); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 3f0826daa6..ae77c048cc 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -4071,15 +4071,15 @@ export const platform_oauth_credentials = pgTable( .notNull() .references(() => platform_integrations.id, { onDelete: 'cascade' }), platform: text(), - authorized_by_user_id: text() - .notNull() - .references(() => kilocode_users.id, { onDelete: 'cascade' }), + authorized_by_user_id: text().references(() => kilocode_users.id, { onDelete: 'cascade' }), provider_subject_id: text().notNull(), provider_subject_login: text().notNull(), + provider_base_url: text(), access_token_encrypted: text().notNull(), access_token_expires_at: timestamp({ withTimezone: true, mode: 'string' }), - refresh_token_encrypted: text().notNull(), + refresh_token_encrypted: text(), refresh_token_expires_at: timestamp({ withTimezone: true, mode: 'string' }), + oauth_client_secret_encrypted: text(), credential_version: integer().notNull().default(1), revoked_at: timestamp({ withTimezone: true, mode: 'string' }), revocation_reason: text(), @@ -4095,6 +4095,10 @@ export const platform_oauth_credentials = pgTable( table.platform_integration_id ), index('IDX_platform_oauth_credentials_authorized_by_user_id').on(table.authorized_by_user_id), + check( + 'platform_oauth_credentials_credential_version_check', + sql`${table.credential_version} > 0` + ), ] ); @@ -4111,11 +4115,17 @@ export const platform_access_token_credentials = pgTable( integration_type: text().$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(), + provider_credential_type: text() + .notNull() + .$type<'workspace_access_token' | 'personal_access_token' | 'project_access_token'>(), + provider_resource_id: text(), + provider_base_url: text(), + authorized_by_user_id: text().references(() => kilocode_users.id, { onDelete: 'cascade' }), + provider_metadata: jsonb(), + provider_scopes: text().array(), + provider_verified_at: timestamp({ withTimezone: true, mode: 'string' }), credential_version: integer().notNull().default(1), - last_validated_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + last_validated_at: timestamp({ withTimezone: true, mode: 'string' }), last_used_at: timestamp({ withTimezone: true, mode: 'string' }), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), updated_at: timestamp({ withTimezone: true, mode: 'string' }) @@ -4124,14 +4134,28 @@ export const platform_access_token_credentials = pgTable( .$onUpdateFn(() => sql`now()`), }, table => [ - unique('UQ_platform_access_token_credentials_platform_integration_id').on( - table.platform_integration_id - ), + uniqueIndex('UQ_platform_access_token_credentials_integration_level') + .on(table.platform_integration_id) + .where(isNull(table.provider_resource_id)), + uniqueIndex('UQ_platform_access_token_credentials_resource') + .on(table.platform_integration_id, table.provider_credential_type, table.provider_resource_id) + .where(isNotNull(table.provider_resource_id)), foreignKey({ columns: [table.platform_integration_id], foreignColumns: [platform_integrations.id], name: 'FK_platform_access_token_credentials_parent', }).onDelete('cascade'), + index('IDX_platform_access_token_credentials_authorized_by_user_id').on( + table.authorized_by_user_id + ), + check( + 'platform_access_token_credentials_credential_version_check', + sql`${table.credential_version} > 0` + ), + check( + 'platform_access_token_credentials_resource_id_check', + sql`${table.provider_resource_id} IS NULL OR ${table.provider_resource_id} <> ''` + ), ] ); diff --git a/packages/worker-utils/package.json b/packages/worker-utils/package.json index 78c91ee1cf..a2f9e691d5 100644 --- a/packages/worker-utils/package.json +++ b/packages/worker-utils/package.json @@ -6,6 +6,7 @@ "exports": { ".": "./src/index.ts", "./bitbucket-workspace-access-token": "./src/bitbucket-workspace-access-token.ts", + "./gitlab-credential": "./src/gitlab-credential.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", diff --git a/packages/worker-utils/src/bitbucket-workspace-access-token.test.ts b/packages/worker-utils/src/bitbucket-workspace-access-token.test.ts index 471c12753e..9034cb0407 100644 --- a/packages/worker-utils/src/bitbucket-workspace-access-token.test.ts +++ b/packages/worker-utils/src/bitbucket-workspace-access-token.test.ts @@ -6,6 +6,8 @@ import { BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_SCOPE_LABELS, BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_EFFECTIVE_SCOPES, + BitbucketOAuthCredentialRowSchema, + BitbucketWorkspaceAccessTokenCredentialRowSchema, buildBitbucketOrganizationCredentialLockKey, buildBitbucketWorkspaceAccessTokenAad, getMissingBitbucketWorkspaceAccessTokenScopes, @@ -24,6 +26,71 @@ const aadInput = { }; describe('Bitbucket Workspace Access Token contract', () => { + it('requires the complete legacy Bitbucket OAuth credential profile', () => { + const row = { + id: 'credential-1', + platform_integration_id: 'integration-1', + authorized_by_user_id: 'user-1', + provider_subject_id: 'provider-user-1', + provider_subject_login: 'octocat', + provider_base_url: null, + access_token_encrypted: 'access-envelope', + access_token_expires_at: '2026-07-13T12:00:00.000Z', + refresh_token_encrypted: 'refresh-envelope', + refresh_token_expires_at: '2026-08-13T12:00:00.000Z', + oauth_client_secret_encrypted: null, + credential_version: 1, + revoked_at: null, + revocation_reason: null, + last_used_at: null, + created_at: '2026-07-13T12:00:00.000Z', + updated_at: '2026-07-13T12:00:00.000Z', + }; + + expect(BitbucketOAuthCredentialRowSchema.parse(row)).toEqual(row); + expect( + BitbucketOAuthCredentialRowSchema.safeParse({ ...row, authorized_by_user_id: null }).success + ).toBe(false); + expect( + BitbucketOAuthCredentialRowSchema.safeParse({ ...row, refresh_token_encrypted: null }).success + ).toBe(false); + }); + + it('requires the complete Bitbucket workspace access-token credential profile', () => { + const row = { + id: 'credential-2', + platform_integration_id: 'integration-2', + token_encrypted: 'token-envelope', + expires_at: null, + provider_credential_type: 'workspace_access_token', + provider_resource_id: null, + provider_base_url: null, + authorized_by_user_id: null, + provider_metadata: null, + provider_scopes: ['account', 'repository', 'repository:write', 'pullrequest', 'webhook'], + provider_verified_at: '2026-07-13T12:00:00.000Z', + credential_version: 2, + last_validated_at: '2026-07-13T12:00:00.000Z', + last_used_at: null, + created_at: '2026-07-13T12:00:00.000Z', + updated_at: '2026-07-13T12:00:00.000Z', + }; + + expect(BitbucketWorkspaceAccessTokenCredentialRowSchema.parse(row)).toEqual(row); + expect( + BitbucketWorkspaceAccessTokenCredentialRowSchema.safeParse({ + ...row, + provider_scopes: null, + }).success + ).toBe(false); + expect( + BitbucketWorkspaceAccessTokenCredentialRowSchema.safeParse({ + ...row, + last_validated_at: null, + }).success + ).toBe(false); + }); + it('builds the compatible organization credential lock key', () => { expect( buildBitbucketOrganizationCredentialLockKey('123e4567-e89b-12d3-a456-426614174030') diff --git a/packages/worker-utils/src/bitbucket-workspace-access-token.ts b/packages/worker-utils/src/bitbucket-workspace-access-token.ts index 349a2291c0..2bb78def5e 100644 --- a/packages/worker-utils/src/bitbucket-workspace-access-token.ts +++ b/packages/worker-utils/src/bitbucket-workspace-access-token.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + 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; @@ -31,6 +33,78 @@ export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_SCOPE_LABELS = { webhook: 'Webhooks Read and Write', } satisfies Record; +const BitbucketIsoTimestampSchema = z.iso.datetime({ offset: true }); +const BitbucketPostgresTimestampPattern = + /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)([+-]\d{2}(?::?\d{2})?)$/; + +const BitbucketCredentialTimestampSchema = z.string().refine(value => { + if (BitbucketIsoTimestampSchema.safeParse(value).success) return true; + + const match = BitbucketPostgresTimestampPattern.exec(value); + if (!match) return false; + const [, date, time, postgresOffset] = match; + const offset = + postgresOffset.length === 3 + ? `${postgresOffset}:00` + : postgresOffset.length === 5 + ? `${postgresOffset.slice(0, 3)}:${postgresOffset.slice(3)}` + : postgresOffset; + return BitbucketIsoTimestampSchema.safeParse(`${date}T${time}${offset}`).success; +}, 'Invalid credential timestamp'); + +export const BitbucketOAuthCredentialRowSchema = z + .object({ + id: z.string().min(1), + platform_integration_id: z.string().min(1), + platform: z.string().nullable().optional(), + authorized_by_user_id: z.string().min(1), + provider_subject_id: z.string().min(1), + provider_subject_login: z.string().min(1), + provider_base_url: z.null(), + access_token_encrypted: z.string().min(1), + access_token_expires_at: BitbucketCredentialTimestampSchema.nullable(), + refresh_token_encrypted: z.string().min(1), + refresh_token_expires_at: BitbucketCredentialTimestampSchema.nullable(), + oauth_client_secret_encrypted: z.null(), + credential_version: z.number().int().positive(), + revoked_at: BitbucketCredentialTimestampSchema.nullable(), + revocation_reason: z.string().nullable(), + last_used_at: BitbucketCredentialTimestampSchema.nullable(), + created_at: BitbucketCredentialTimestampSchema, + updated_at: BitbucketCredentialTimestampSchema, + }) + .strict(); + +export type BitbucketOAuthCredentialRow = z.infer; + +export const BitbucketWorkspaceAccessTokenCredentialRowSchema = z + .object({ + id: z.string().min(1), + platform_integration_id: z.string().min(1), + owned_by_organization_id: z.string().nullable().optional(), + platform: z.string().nullable().optional(), + integration_type: z.string().nullable().optional(), + token_encrypted: z.string().min(1), + expires_at: BitbucketCredentialTimestampSchema.nullable(), + provider_credential_type: z.literal(BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE), + provider_resource_id: z.null(), + provider_base_url: z.null(), + authorized_by_user_id: z.null(), + provider_metadata: z.null(), + provider_scopes: z.array(z.string().min(1)), + provider_verified_at: BitbucketCredentialTimestampSchema, + credential_version: z.number().int().positive(), + last_validated_at: BitbucketCredentialTimestampSchema, + last_used_at: BitbucketCredentialTimestampSchema.nullable(), + created_at: BitbucketCredentialTimestampSchema, + updated_at: BitbucketCredentialTimestampSchema, + }) + .strict(); + +export type BitbucketWorkspaceAccessTokenCredentialRow = z.infer< + typeof BitbucketWorkspaceAccessTokenCredentialRowSchema +>; + export function buildBitbucketOrganizationCredentialLockKey(organizationId: string): string { return `bitbucket-oauth-owner:org:${organizationId}`; } diff --git a/packages/worker-utils/src/gitlab-credential.test.ts b/packages/worker-utils/src/gitlab-credential.test.ts new file mode 100644 index 0000000000..5afab1405e --- /dev/null +++ b/packages/worker-utils/src/gitlab-credential.test.ts @@ -0,0 +1,353 @@ +import { describe, expect, it } from 'vitest'; +import { + GitLabProjectAccessTokenMetadataSchema as RootGitLabProjectAccessTokenMetadataSchema, + buildGitLabOAuthCredentialAad as rootBuildGitLabOAuthCredentialAad, +} from './index'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GitLabOAuthCredentialRowSchema, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GitLabPersonalAccessTokenCredentialRowSchema, + GitLabPersonalAccessTokenMetadataSchema, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + GitLabProjectAccessTokenCredentialRowSchema, + GitLabProjectAccessTokenMetadataSchema, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, +} from './gitlab-credential'; + +describe('GitLab credential contract', () => { + it('exposes distinct envelope schemes for every credential kind', () => { + expect(GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME).toBe('gitlab-oauth-credential-rsa-aes-256-gcm'); + expect(GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME).toBe( + 'gitlab-personal-access-token-rsa-aes-256-gcm' + ); + expect(GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME).toBe( + 'gitlab-project-access-token-rsa-aes-256-gcm' + ); + }); + + it('builds OAuth AAD from the credential context, generation, and secret kind', () => { + const input = { + credentialId: 'credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com/root', + owner: { type: 'org', id: 'organization-1' } as const, + authorizedByUserId: null, + kind: 'oauth-client-secret' as const, + credentialVersion: 7, + providerSubjectId: 'must-not-be-bound', + }; + + expect(buildGitLabOAuthCredentialAad(input)).toBe( + JSON.stringify({ + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + version: 1, + platform: 'gitlab', + credentialId: 'credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com/root', + owner: { type: 'org', id: 'organization-1' }, + authorizedByUserId: null, + credentialVersion: 7, + kind: 'oauth-client-secret', + }) + ); + expect(buildGitLabOAuthCredentialAad(input)).not.toContain('must-not-be-bound'); + }); + + it('builds PAT AAD with the supplier and credential generation but no integration type', () => { + const input = { + credentialId: 'credential-2', + integrationId: 'integration-2', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-1' } as const, + authorizedByUserId: 'user-1', + credentialVersion: 4, + integrationType: 'must-not-be-bound', + }; + + const aad = buildGitLabPersonalAccessTokenAad(input); + expect(aad).toBe( + JSON.stringify({ + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + version: 1, + platform: 'gitlab', + credentialId: 'credential-2', + integrationId: 'integration-2', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-1' }, + providerCredentialType: 'personal_access_token', + providerResourceId: null, + authorizedByUserId: 'user-1', + credentialVersion: 4, + }) + ); + expect(aad).not.toContain('must-not-be-bound'); + }); + + it('builds project-token AAD with its resource but without a primary-token supplier', () => { + const input = { + credentialId: 'credential-3', + integrationId: 'integration-3', + providerBaseUrl: 'https://gitlab.example.com/group', + owner: { type: 'org', id: 'organization-2' } as const, + providerResourceId: '42', + credentialVersion: 2, + authorizedByUserId: 'must-not-be-bound', + integrationType: 'must-also-not-be-bound', + }; + + const aad = buildGitLabProjectAccessTokenAad(input); + expect(aad).toBe( + JSON.stringify({ + scheme: GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + version: 1, + platform: 'gitlab', + credentialId: 'credential-3', + integrationId: 'integration-3', + providerBaseUrl: 'https://gitlab.example.com/group', + owner: { type: 'org', id: 'organization-2' }, + providerCredentialType: 'project_access_token', + providerResourceId: '42', + credentialVersion: 2, + }) + ); + expect(aad).not.toContain('must-not-be-bound'); + }); + + it('normalizes owner property order for every credential AAD', () => { + const reversedOwner = { id: 'organization-1', type: 'org' } as const; + const normalizedOwner = { type: 'org', id: 'organization-1' } as const; + + expect( + buildGitLabOAuthCredentialAad({ + credentialId: 'oauth-credential', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: reversedOwner, + authorizedByUserId: 'user-1', + credentialVersion: 1, + kind: 'access', + }) + ).toBe( + buildGitLabOAuthCredentialAad({ + credentialId: 'oauth-credential', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: normalizedOwner, + authorizedByUserId: 'user-1', + credentialVersion: 1, + kind: 'access', + }) + ); + expect( + buildGitLabPersonalAccessTokenAad({ + credentialId: 'pat-credential', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: reversedOwner, + authorizedByUserId: 'user-1', + credentialVersion: 1, + }) + ).toBe( + buildGitLabPersonalAccessTokenAad({ + credentialId: 'pat-credential', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: normalizedOwner, + authorizedByUserId: 'user-1', + credentialVersion: 1, + }) + ); + expect( + buildGitLabProjectAccessTokenAad({ + credentialId: 'project-credential', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: reversedOwner, + providerResourceId: '42', + credentialVersion: 1, + }) + ).toBe( + buildGitLabProjectAccessTokenAad({ + credentialId: 'project-credential', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: normalizedOwner, + providerResourceId: '42', + credentialVersion: 1, + }) + ); + }); + + it('accepts only the optional legacy-safe PAT provider metadata', () => { + expect(GitLabPersonalAccessTokenMetadataSchema.parse({})).toEqual({}); + expect( + GitLabPersonalAccessTokenMetadataSchema.parse({ + providerCredentialId: '123456', + expiresOn: '2030-12-31', + }) + ).toEqual({ providerCredentialId: '123456', expiresOn: '2030-12-31' }); + + expect( + GitLabPersonalAccessTokenMetadataSchema.safeParse({ providerCredentialId: '0' }).success + ).toBe(false); + expect( + GitLabPersonalAccessTokenMetadataSchema.safeParse({ expiresOn: '2030-02-30' }).success + ).toBe(false); + expect(GitLabPersonalAccessTokenMetadataSchema.safeParse({ tokenName: 'secret' }).success).toBe( + false + ); + }); + + it('requires both provider identifiers for project-token metadata', () => { + expect( + GitLabProjectAccessTokenMetadataSchema.parse({ + providerCredentialId: '987', + expiresOn: '2031-01-15', + }) + ).toEqual({ providerCredentialId: '987', expiresOn: '2031-01-15' }); + + expect( + GitLabProjectAccessTokenMetadataSchema.safeParse({ providerCredentialId: '987' }).success + ).toBe(false); + expect( + GitLabProjectAccessTokenMetadataSchema.safeParse({ expiresOn: '2031-01-15' }).success + ).toBe(false); + expect( + GitLabProjectAccessTokenMetadataSchema.safeParse({ + providerCredentialId: '987', + expiresOn: '2031-01-15', + token: 'must-never-be-stored', + }).success + ).toBe(false); + }); + + it('validates the complete GitLab OAuth row without normalizing PostgreSQL timestamps', () => { + const postgresTimestamp = '2026-04-29 01:16:12.945+00'; + const row = { + id: 'credential-1', + platform_integration_id: 'integration-1', + authorized_by_user_id: null, + provider_subject_id: '123', + provider_subject_login: 'octocat', + provider_base_url: 'https://gitlab.example.com/root', + access_token_encrypted: 'access-envelope', + access_token_expires_at: postgresTimestamp, + refresh_token_encrypted: null, + refresh_token_expires_at: null, + oauth_client_secret_encrypted: 'client-secret-envelope', + credential_version: 3, + revoked_at: null, + revocation_reason: null, + last_used_at: null, + created_at: postgresTimestamp, + updated_at: postgresTimestamp, + }; + + expect(GitLabOAuthCredentialRowSchema.parse(row)).toEqual(row); + expect( + GitLabOAuthCredentialRowSchema.safeParse({ ...row, platform: 'bitbucket' }).success + ).toBe(true); + expect( + GitLabOAuthCredentialRowSchema.safeParse({ ...row, provider_base_url: null }).success + ).toBe(false); + for (const providerBaseUrl of [ + 'http://gitlab.example.com', + 'https://user@gitlab.example.com', + 'https://gitlab.example.com?group=one', + 'https://gitlab.example.com#group', + 'https://GitLab.Example.com/', + ]) { + expect( + GitLabOAuthCredentialRowSchema.safeParse({ + ...row, + provider_base_url: providerBaseUrl, + }).success + ).toBe(false); + } + expect(GitLabOAuthCredentialRowSchema.safeParse({ ...row, unexpected: true }).success).toBe( + false + ); + }); + + it('validates an integration-level GitLab PAT row profile', () => { + const row = { + id: 'credential-2', + platform_integration_id: 'integration-2', + token_encrypted: 'pat-envelope', + expires_at: null, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: 'https://gitlab.example.com', + authorized_by_user_id: null, + provider_metadata: {}, + provider_scopes: null, + provider_verified_at: null, + credential_version: 1, + last_validated_at: null, + last_used_at: null, + created_at: '2026-07-13T12:00:00.000Z', + updated_at: '2026-07-13T12:00:00.000Z', + }; + + expect(GitLabPersonalAccessTokenCredentialRowSchema.parse(row)).toEqual(row); + expect( + GitLabPersonalAccessTokenCredentialRowSchema.safeParse({ + ...row, + provider_resource_id: '42', + }).success + ).toBe(false); + expect( + GitLabPersonalAccessTokenCredentialRowSchema.safeParse({ + ...row, + provider_metadata: { token: 'must-never-be-stored' }, + }).success + ).toBe(false); + }); + + it('validates a resource-scoped GitLab project-token row profile', () => { + const row = { + id: 'credential-3', + platform_integration_id: 'integration-3', + token_encrypted: 'project-token-envelope', + expires_at: null, + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + provider_base_url: 'https://gitlab.example.com/root', + authorized_by_user_id: null, + provider_metadata: { + providerCredentialId: '654', + expiresOn: '2032-06-30', + }, + provider_scopes: ['api'], + provider_verified_at: '2026-07-13T12:00:00.000Z', + credential_version: 5, + last_validated_at: '2026-07-13T12:00:00.000Z', + last_used_at: null, + created_at: '2026-07-13T12:00:00.000Z', + updated_at: '2026-07-13T12:00:00.000Z', + }; + + expect(GitLabProjectAccessTokenCredentialRowSchema.parse(row)).toEqual(row); + expect( + GitLabProjectAccessTokenCredentialRowSchema.safeParse({ + ...row, + provider_resource_id: 'not-decimal', + }).success + ).toBe(false); + expect( + GitLabProjectAccessTokenCredentialRowSchema.safeParse({ + ...row, + authorized_by_user_id: 'user-1', + }).success + ).toBe(false); + }); + + it('exports the GitLab credential contract from the package root', () => { + expect(rootBuildGitLabOAuthCredentialAad).toBe(buildGitLabOAuthCredentialAad); + expect(RootGitLabProjectAccessTokenMetadataSchema).toBe(GitLabProjectAccessTokenMetadataSchema); + }); +}); diff --git a/packages/worker-utils/src/gitlab-credential.ts b/packages/worker-utils/src/gitlab-credential.ts new file mode 100644 index 0000000000..83200db8c0 --- /dev/null +++ b/packages/worker-utils/src/gitlab-credential.ts @@ -0,0 +1,258 @@ +import { z } from 'zod'; + +export const GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME = 'gitlab-oauth-credential-rsa-aes-256-gcm'; +export const GITLAB_OAUTH_CREDENTIAL_ENVELOPE_VERSION = 1; +export const GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME = + 'gitlab-personal-access-token-rsa-aes-256-gcm'; +export const GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_VERSION = 1; +export const GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME = + 'gitlab-project-access-token-rsa-aes-256-gcm'; +export const GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_VERSION = 1; + +const GitLabPositiveDecimalIdSchema = z.string().regex(/^[1-9][0-9]*$/); + +export const GitLabPersonalAccessTokenMetadataSchema = z + .object({ + providerCredentialId: GitLabPositiveDecimalIdSchema.optional(), + expiresOn: z.iso.date().optional(), + }) + .strict(); + +export type GitLabPersonalAccessTokenMetadata = z.infer< + typeof GitLabPersonalAccessTokenMetadataSchema +>; + +export const GitLabProjectAccessTokenMetadataSchema = z + .object({ + providerCredentialId: GitLabPositiveDecimalIdSchema, + expiresOn: z.iso.date(), + }) + .strict(); + +export type GitLabProjectAccessTokenMetadata = z.infer< + typeof GitLabProjectAccessTokenMetadataSchema +>; + +const GitLabIsoTimestampSchema = z.iso.datetime({ offset: true }); +const GitLabPostgresTimestampPattern = + /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)([+-]\d{2}(?::?\d{2})?)$/; + +function isCanonicalGitLabProviderBaseUrl(value: string): boolean { + try { + const parsed = new URL(value); + const rawPath = /^https:\/\/[^/]*(\/[^?#]*)?/.exec(value)?.[1] ?? ''; + if ( + parsed.protocol !== 'https:' || + !parsed.hostname || + parsed.username !== '' || + parsed.password !== '' || + parsed.search !== '' || + parsed.hash !== '' || + rawPath.includes('\\') || + /%2f|%5c/i.test(rawPath) || + /\/\//.test(rawPath) || + /\/(?:(?:\.|%2e){1,2})(?:\/|$)/i.test(rawPath) + ) { + return false; + } + + const basePath = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/$/, ''); + return value === `${parsed.origin}${basePath}`; + } catch { + return false; + } +} + +const GitLabProviderBaseUrlSchema = z + .string() + .refine(isCanonicalGitLabProviderBaseUrl, 'Invalid canonical GitLab provider base URL'); + +const GitLabCredentialTimestampSchema = z.string().refine(value => { + if (GitLabIsoTimestampSchema.safeParse(value).success) return true; + + const match = GitLabPostgresTimestampPattern.exec(value); + if (!match) return false; + const [, date, time, postgresOffset] = match; + const offset = + postgresOffset.length === 3 + ? `${postgresOffset}:00` + : postgresOffset.length === 5 + ? `${postgresOffset.slice(0, 3)}:${postgresOffset.slice(3)}` + : postgresOffset; + return GitLabIsoTimestampSchema.safeParse(`${date}T${time}${offset}`).success; +}, 'Invalid credential timestamp'); + +export const GitLabOAuthCredentialRowSchema = z + .object({ + id: z.string().min(1), + platform_integration_id: z.string().min(1), + platform: z.string().nullable().optional(), + authorized_by_user_id: z.string().min(1).nullable(), + provider_subject_id: z.string().min(1), + provider_subject_login: z.string().min(1), + provider_base_url: GitLabProviderBaseUrlSchema, + access_token_encrypted: z.string().min(1), + access_token_expires_at: GitLabCredentialTimestampSchema.nullable(), + refresh_token_encrypted: z.string().min(1).nullable(), + refresh_token_expires_at: GitLabCredentialTimestampSchema.nullable(), + oauth_client_secret_encrypted: z.string().min(1).nullable(), + credential_version: z.number().int().positive(), + revoked_at: GitLabCredentialTimestampSchema.nullable(), + revocation_reason: z.string().nullable(), + last_used_at: GitLabCredentialTimestampSchema.nullable(), + created_at: GitLabCredentialTimestampSchema, + updated_at: GitLabCredentialTimestampSchema, + }) + .strict(); + +export type GitLabOAuthCredentialRow = z.infer; + +const GitLabAccessTokenCredentialRowBaseSchema = z.object({ + id: z.string().min(1), + platform_integration_id: z.string().min(1), + owned_by_organization_id: z.string().nullable().optional(), + platform: z.string().nullable().optional(), + integration_type: z.string().nullable().optional(), + token_encrypted: z.string().min(1), + expires_at: z.null(), + provider_base_url: GitLabProviderBaseUrlSchema, + provider_scopes: z.array(z.string().min(1)).nullable(), + provider_verified_at: GitLabCredentialTimestampSchema.nullable(), + credential_version: z.number().int().positive(), + last_validated_at: GitLabCredentialTimestampSchema.nullable(), + last_used_at: GitLabCredentialTimestampSchema.nullable(), + created_at: GitLabCredentialTimestampSchema, + updated_at: GitLabCredentialTimestampSchema, +}); + +export const GitLabPersonalAccessTokenCredentialRowSchema = + GitLabAccessTokenCredentialRowBaseSchema.extend({ + provider_credential_type: z.literal('personal_access_token'), + provider_resource_id: z.null(), + authorized_by_user_id: z.string().min(1).nullable(), + provider_metadata: GitLabPersonalAccessTokenMetadataSchema, + }).strict(); + +export type GitLabPersonalAccessTokenCredentialRow = z.infer< + typeof GitLabPersonalAccessTokenCredentialRowSchema +>; + +export const GitLabProjectAccessTokenCredentialRowSchema = + GitLabAccessTokenCredentialRowBaseSchema.extend({ + provider_credential_type: z.literal('project_access_token'), + provider_resource_id: GitLabPositiveDecimalIdSchema, + authorized_by_user_id: z.null(), + provider_metadata: GitLabProjectAccessTokenMetadataSchema, + }).strict(); + +export type GitLabProjectAccessTokenCredentialRow = z.infer< + typeof GitLabProjectAccessTokenCredentialRowSchema +>; + +export type GitLabCredentialOwner = { type: 'user'; id: string } | { type: 'org'; id: string }; + +function normalizeGitLabCredentialOwner(owner: GitLabCredentialOwner): GitLabCredentialOwner { + return { type: owner.type, id: owner.id }; +} + +export type GitLabOAuthSecretKind = 'access' | 'refresh' | 'oauth-client-secret'; + +export type GitLabOAuthCredentialAadInput = { + credentialId: string; + integrationId: string; + providerBaseUrl: string; + owner: GitLabCredentialOwner; + authorizedByUserId: string | null; + credentialVersion: number; + kind: GitLabOAuthSecretKind; +}; + +export function buildGitLabOAuthCredentialAad(input: GitLabOAuthCredentialAadInput): string { + return JSON.stringify({ + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + version: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_VERSION, + platform: 'gitlab', + credentialId: input.credentialId, + integrationId: input.integrationId, + providerBaseUrl: input.providerBaseUrl, + owner: normalizeGitLabCredentialOwner(input.owner), + authorizedByUserId: input.authorizedByUserId, + credentialVersion: input.credentialVersion, + kind: input.kind, + }); +} + +type GitLabAccessTokenCredentialAadBaseInput = { + credentialId: string; + integrationId: string; + providerBaseUrl: string; + owner: GitLabCredentialOwner; + credentialVersion: number; +}; + +export type GitLabPersonalAccessTokenAadInput = GitLabAccessTokenCredentialAadBaseInput & { + authorizedByUserId: string | null; +}; + +export type GitLabProjectAccessTokenAadInput = GitLabAccessTokenCredentialAadBaseInput & { + providerResourceId: string; +}; + +export type GitLabAccessTokenCredentialAadInput = + | (GitLabPersonalAccessTokenAadInput & { + providerCredentialType: 'personal_access_token'; + providerResourceId: null; + }) + | (GitLabProjectAccessTokenAadInput & { + providerCredentialType: 'project_access_token'; + }); + +export function buildGitLabAccessTokenCredentialAad( + input: GitLabAccessTokenCredentialAadInput +): string { + if (input.providerCredentialType === 'project_access_token') { + return JSON.stringify({ + scheme: GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + version: GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_VERSION, + platform: 'gitlab', + credentialId: input.credentialId, + integrationId: input.integrationId, + providerBaseUrl: input.providerBaseUrl, + owner: normalizeGitLabCredentialOwner(input.owner), + providerCredentialType: input.providerCredentialType, + providerResourceId: input.providerResourceId, + credentialVersion: input.credentialVersion, + }); + } + + return JSON.stringify({ + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + version: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_VERSION, + platform: 'gitlab', + credentialId: input.credentialId, + integrationId: input.integrationId, + providerBaseUrl: input.providerBaseUrl, + owner: normalizeGitLabCredentialOwner(input.owner), + providerCredentialType: input.providerCredentialType, + providerResourceId: input.providerResourceId, + authorizedByUserId: input.authorizedByUserId, + credentialVersion: input.credentialVersion, + }); +} + +export function buildGitLabPersonalAccessTokenAad( + input: GitLabPersonalAccessTokenAadInput +): string { + return buildGitLabAccessTokenCredentialAad({ + ...input, + providerCredentialType: 'personal_access_token', + providerResourceId: null, + }); +} + +export function buildGitLabProjectAccessTokenAad(input: GitLabProjectAccessTokenAadInput): string { + return buildGitLabAccessTokenCredentialAad({ + ...input, + providerCredentialType: 'project_access_token', + }); +} diff --git a/packages/worker-utils/src/index.ts b/packages/worker-utils/src/index.ts index 1462f54365..8244d4a7c8 100644 --- a/packages/worker-utils/src/index.ts +++ b/packages/worker-utils/src/index.ts @@ -51,7 +51,11 @@ 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 { + BITBUCKET_REPOSITORY_LIST_AUDIENCE, + GITLAB_CREDENTIAL_AUDIT_AUDIENCE, + GITLAB_CREDENTIAL_BROKER_AUDIENCE, +} from './internal-service-token-audiences.js'; export { BITBUCKET_ACCESS_TOKEN_FAMILY_PREFIX, BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, @@ -71,6 +75,36 @@ export type { BitbucketWorkspaceAccessTokenInvalidationReason, BitbucketWorkspaceAccessTokenRequiredScope, } from './bitbucket-workspace-access-token.js'; +export { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_VERSION, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_VERSION, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_VERSION, + GitLabOAuthCredentialRowSchema, + GitLabPersonalAccessTokenCredentialRowSchema, + GitLabPersonalAccessTokenMetadataSchema, + GitLabProjectAccessTokenCredentialRowSchema, + GitLabProjectAccessTokenMetadataSchema, + buildGitLabAccessTokenCredentialAad, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, +} from './gitlab-credential.js'; +export type { + GitLabAccessTokenCredentialAadInput, + GitLabCredentialOwner, + GitLabOAuthCredentialAadInput, + GitLabOAuthCredentialRow, + GitLabOAuthSecretKind, + GitLabPersonalAccessTokenAadInput, + GitLabPersonalAccessTokenCredentialRow, + GitLabPersonalAccessTokenMetadata, + GitLabProjectAccessTokenAadInput, + GitLabProjectAccessTokenCredentialRow, + GitLabProjectAccessTokenMetadata, +} from './gitlab-credential.js'; export { signKiloToken, verifyKiloToken, diff --git a/packages/worker-utils/src/internal-service-token-audiences.test.ts b/packages/worker-utils/src/internal-service-token-audiences.test.ts index a37c476490..75b9471f78 100644 --- a/packages/worker-utils/src/internal-service-token-audiences.test.ts +++ b/packages/worker-utils/src/internal-service-token-audiences.test.ts @@ -1,9 +1,15 @@ import { describe, expect, it } from 'vitest'; +import { + GITLAB_CREDENTIAL_AUDIT_AUDIENCE as RootGitLabCredentialAuditAudience, + GITLAB_CREDENTIAL_BROKER_AUDIENCE as RootGitLabCredentialBrokerAudience, +} from './index.js'; import { BITBUCKET_CODE_REVIEW_PULL_REQUEST_AUDIENCE, BITBUCKET_CODE_REVIEW_WEBHOOK_DELETE_AUDIENCE, BITBUCKET_CODE_REVIEW_WEBHOOK_ENSURE_AUDIENCE, BITBUCKET_REPOSITORY_LIST_AUDIENCE, + GITLAB_CREDENTIAL_BROKER_AUDIENCE, + GITLAB_CREDENTIAL_AUDIT_AUDIENCE, } from './internal-service-token-audiences.js'; describe('internal service token audiences', () => { @@ -24,4 +30,15 @@ describe('internal service token audiences', () => { ]) ); }); + + it('exports one purpose-bound GitLab credential broker audience', () => { + expect(GITLAB_CREDENTIAL_BROKER_AUDIENCE).toBe('git-token-service:gitlab-credentials'); + expect(RootGitLabCredentialBrokerAudience).toBe(GITLAB_CREDENTIAL_BROKER_AUDIENCE); + }); + + it('exports a distinct purpose-bound GitLab credential audit audience', () => { + expect(GITLAB_CREDENTIAL_AUDIT_AUDIENCE).toBe('git-token-service:gitlab-credential-audit'); + expect(GITLAB_CREDENTIAL_AUDIT_AUDIENCE).not.toBe(GITLAB_CREDENTIAL_BROKER_AUDIENCE); + expect(RootGitLabCredentialAuditAudience).toBe(GITLAB_CREDENTIAL_AUDIT_AUDIENCE); + }); }); diff --git a/packages/worker-utils/src/internal-service-token-audiences.ts b/packages/worker-utils/src/internal-service-token-audiences.ts index d89b1d3cf1..6b90139d0c 100644 --- a/packages/worker-utils/src/internal-service-token-audiences.ts +++ b/packages/worker-utils/src/internal-service-token-audiences.ts @@ -5,3 +5,5 @@ export const BITBUCKET_CODE_REVIEW_WEBHOOK_ENSURE_AUDIENCE = 'git-token-service:bitbucket-code-review:webhook-ensure'; export const BITBUCKET_CODE_REVIEW_WEBHOOK_DELETE_AUDIENCE = 'git-token-service:bitbucket-code-review:webhook-delete'; +export const GITLAB_CREDENTIAL_BROKER_AUDIENCE = 'git-token-service:gitlab-credentials'; +export const GITLAB_CREDENTIAL_AUDIT_AUDIENCE = 'git-token-service:gitlab-credential-audit'; diff --git a/services/git-token-service/.dev.vars.example b/services/git-token-service/.dev.vars.example index 1bfac1c6ee..b2329f4106 100644 --- a/services/git-token-service/.dev.vars.example +++ b/services/git-token-service/.dev.vars.example @@ -39,12 +39,13 @@ 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. +# Shared RSA/AES platform-credential envelope keypair for persisted Bitbucket and GitLab 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. +# The BITBUCKET-prefixed names are legacy; use the same key ID and public key in Web and keep +# the private key in this Worker only. # Provision these as per-Worker secrets; the key material exceeds Secrets Store's size limit. BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID= BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY= diff --git a/services/git-token-service/src/bitbucket-authorization-service.test.ts b/services/git-token-service/src/bitbucket-authorization-service.test.ts index bcae4b605d..f2c0d43198 100644 --- a/services/git-token-service/src/bitbucket-authorization-service.test.ts +++ b/services/git-token-service/src/bitbucket-authorization-service.test.ts @@ -77,7 +77,11 @@ const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toStri type TestOwner = { type: 'user' | 'org'; id: string }; -function aad(kind: 'access' | 'refresh', owner: TestOwner = { type: 'user', id: 'user-1' }) { +function aad( + kind: 'access' | 'refresh', + owner: TestOwner = { type: 'user', id: 'user-1' }, + authorizedByUserId: string | null = 'user-1' +) { return JSON.stringify({ scheme, version: 1, @@ -85,7 +89,7 @@ function aad(kind: 'access' | 'refresh', owner: TestOwner = { type: 'user', id: credentialId: 'credential-1', integrationId: 'integration-1', owner, - authorizedByUserId: 'user-1', + authorizedByUserId, kind, }); } @@ -98,10 +102,10 @@ function credential( 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', + provider_base_url: null, access_token_encrypted: encryptKeyedEnvelope( 'access-token', scheme, @@ -116,6 +120,7 @@ function credential( aad('refresh', owner) ), refresh_token_expires_at: null, + oauth_client_secret_encrypted: null, credential_version: 1, revoked_at: null, revocation_reason: null, @@ -189,6 +194,27 @@ describe('BitbucketAuthorizationService', () => { }); }); + it('fails closed before using a nullable OAuth authorizer', async () => { + const row = activeRow(); + database.row = { + ...row, + credential: { + ...row.credential, + authorized_by_user_id: null, + access_token_encrypted: encryptKeyedEnvelope( + 'access-token', + scheme, + { keyId: 'active', publicKeyPem }, + aad('access', undefined, null) + ), + }, + }; + + await expect(service().getAuthorization({ userId: 'user-1' })).resolves.toEqual({ + status: 'reconnect_required', + }); + }); + it('requires stored OAuth credentials to include webhook scope', async () => { database.row = { ...activeRow(), diff --git a/services/git-token-service/src/bitbucket-authorization-service.ts b/services/git-token-service/src/bitbucket-authorization-service.ts index ec15b3b4e8..6b68b83ccf 100644 --- a/services/git-token-service/src/bitbucket-authorization-service.ts +++ b/services/git-token-service/src/bitbucket-authorization-service.ts @@ -11,6 +11,10 @@ import { platform_integrations, platform_oauth_credentials, } from '@kilocode/db/schema'; +import { + BitbucketOAuthCredentialRowSchema, + type BitbucketOAuthCredentialRow, +} from '@kilocode/worker-utils/bitbucket-workspace-access-token'; import { and, eq, exists, isNull, or, sql } from 'drizzle-orm'; import { z } from 'zod'; @@ -68,7 +72,6 @@ export type BitbucketAuthorizationResult = | { 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; @@ -81,7 +84,7 @@ type BitbucketAuthorizationEnv = Pick & { }; type ActiveAuthorization = { - credential: CredentialRow; + credential: BitbucketOAuthCredentialRow; integrationId: string; owner: BitbucketAuthorizationOwner; scopes: string[] | null; @@ -191,10 +194,7 @@ export function buildBitbucketAuthorizationQuery( .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) - ) + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id) ) .innerJoin( kilocode_users, @@ -211,7 +211,7 @@ export function buildBitbucketAuthorizationQuery( } function credentialAad( - credential: CredentialRow, + credential: BitbucketOAuthCredentialRow, owner: BitbucketAuthorizationOwner, kind: 'access' | 'refresh' ): string { @@ -271,7 +271,10 @@ export class BitbucketAuthorizationService { > { const [row] = await buildBitbucketAuthorizationQuery(db, owner); if (!row) return { status: 'not_connected' }; - if (!row.credential || row.credential.revoked_at) return { status: 'reconnect_required' }; + const credential = BitbucketOAuthCredentialRowSchema.safeParse(row.credential); + if (!credential.success || credential.data.revoked_at) { + return { status: 'reconnect_required' }; + } const metadata = MetadataSchema.safeParse(row.metadata); if (!metadata.success) return { status: 'reconnect_required' }; @@ -294,7 +297,7 @@ export class BitbucketAuthorizationService { return { status: 'available', authorization: { - credential: row.credential, + credential: credential.data, integrationId: row.integrationId, owner, scopes: row.scopes, @@ -427,6 +430,8 @@ export class BitbucketAuthorizationService { ) .returning(); if (!updated) return this.loadAuthorization(tx, authorization.owner); + const credential = BitbucketOAuthCredentialRowSchema.safeParse(updated); + if (!credential.success) return { status: 'reconnect_required' }; await tx .update(platform_integrations) @@ -434,7 +439,7 @@ export class BitbucketAuthorizationService { .where(eq(platform_integrations.id, authorization.integrationId)); return { status: 'available', - authorization: { ...authorization, credential: updated, scopes }, + authorization: { ...authorization, credential: credential.data, scopes }, }; } 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 index c6d6565b6f..b42f83d96e 100644 --- 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 @@ -127,17 +127,32 @@ class StatefulAuthorizationStore implements BitbucketWorkspaceAccessTokenAuthori } } -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 } - ); +type TestAuthorizationEnv = ConstructorParameters< + typeof BitbucketWorkspaceAccessTokenAuthorizationService +>[0]; + +function service( + store: StatefulAuthorizationStore, + envOverrides: Partial = {} +) { + const env: TestAuthorizationEnv = { + HYPERDRIVE: { + connectionString: 'postgres://unused', + host: 'unused', + port: 5432, + user: 'unused', + password: 'unused', + database: 'unused', + connect: () => { + throw new Error('unused in this test'); + }, + }, + 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, + }; + return new BitbucketWorkspaceAccessTokenAuthorizationService(env, { store, now: () => now }); } describe('BitbucketWorkspaceAccessTokenAuthorizationService', () => { @@ -151,12 +166,7 @@ describe('BitbucketWorkspaceAccessTokenAuthorizationService', () => { expect(query.sql).toContain( '"platform_access_token_credentials"."platform_integration_id" = "platform_integrations"."id"' ); - expect(query.sql).not.toContain('"platform_access_token_credentials"."expires_at"'); - expect(query.sql).not.toContain( - '"platform_access_token_credentials"."owned_by_organization_id"' - ); - expect(query.sql).not.toContain('"platform_access_token_credentials"."platform"'); - expect(query.sql).not.toContain('"platform_access_token_credentials"."integration_type"'); + expect(query.sql).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"'); 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 index 094695b05e..0011d2078b 100644 --- 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 @@ -14,6 +14,7 @@ import { BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, buildBitbucketOrganizationCredentialLockKey, buildBitbucketWorkspaceAccessTokenAad, + BitbucketWorkspaceAccessTokenCredentialRowSchema, hasBitbucketAccessTokenFamilyPrefix, hasRequiredBitbucketWorkspaceAccessTokenScopes, normalizeBitbucketWorkspaceAccessTokenScopes, @@ -179,7 +180,6 @@ export function buildBitbucketWorkspaceAccessTokenAuthorizationQuery( 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, @@ -189,12 +189,7 @@ export function buildBitbucketWorkspaceAccessTokenAuthorizationQuery( accountId: platform_integrations.platform_account_id, accountLogin: platform_integrations.platform_account_login, authInvalidAt: platform_integrations.auth_invalid_at, - 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, + credential: platform_access_token_credentials, }) .from(platform_integrations) .innerJoin( @@ -318,7 +313,34 @@ class DrizzleBitbucketWorkspaceAccessTokenAuthorizationStore implements Bitbucke }): Promise { const [candidate] = await buildBitbucketWorkspaceAccessTokenAuthorizationQuery(this.db, input); if (!candidate?.organizationId) return null; - return { ...candidate, organizationId: candidate.organizationId }; + const credential = BitbucketWorkspaceAccessTokenCredentialRowSchema.safeParse( + candidate.credential + ); + if ( + !credential.success || + credential.data.platform_integration_id !== candidate.integrationId + ) { + return null; + } + return { + integrationId: candidate.integrationId, + credentialId: credential.data.id, + organizationId: candidate.organizationId, + ownedByUserId: candidate.ownedByUserId, + platform: candidate.platform, + integrationType: candidate.integrationType, + integrationStatus: candidate.integrationStatus, + installationId: candidate.installationId, + accountId: candidate.accountId, + accountLogin: candidate.accountLogin, + authInvalidAt: candidate.authInvalidAt, + tokenEncrypted: credential.data.token_encrypted, + providerCredentialType: credential.data.provider_credential_type, + providerScopes: credential.data.provider_scopes, + providerVerifiedAt: credential.data.provider_verified_at, + credentialVersion: credential.data.credential_version, + lastValidatedAt: credential.data.last_validated_at, + }; } async markUsed( diff --git a/services/git-token-service/src/gitlab-credential-audit-handler.test.ts b/services/git-token-service/src/gitlab-credential-audit-handler.test.ts new file mode 100644 index 0000000000..6ff9c36690 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-audit-handler.test.ts @@ -0,0 +1,19 @@ +import { getWorkerDb } from '@kilocode/db/client'; +import { describe, expect, it } from 'vitest'; +import { buildGitLabCredentialAuditAdminQuery } from './gitlab-credential-audit-handler.js'; + +describe('GitLab credential audit operator authorization', () => { + it('requires the exact caller to be both an admin and unblocked in the database', () => { + const query = buildGitLabCredentialAuditAdminQuery( + getWorkerDb('postgres://query-builder'), + 'admin-1' + ).toSQL(); + + expect(query.sql).toContain('from "kilocode_users"'); + expect(query.sql).toContain('"kilocode_users"."id" ='); + expect(query.sql).toContain('"kilocode_users"."is_admin" ='); + expect(query.sql).toContain('"kilocode_users"."blocked_reason" is null'); + expect(query.params).toContain('admin-1'); + expect(query.params).toContain(true); + }); +}); diff --git a/services/git-token-service/src/gitlab-credential-audit-handler.ts b/services/git-token-service/src/gitlab-credential-audit-handler.ts new file mode 100644 index 0000000000..c488d52367 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-audit-handler.ts @@ -0,0 +1,41 @@ +import { getWorkerDb, type WorkerDb } from '@kilocode/db/client'; +import { kilocode_users } from '@kilocode/db/schema'; +import { and, eq, isNull } from 'drizzle-orm'; +import { + DrizzleGitLabCredentialAuditStore, + GitLabCredentialAuditService, + type GitLabCredentialAuditRequest, +} from './gitlab-credential-audit.js'; +import { GitLabCredentialCrypto } from './gitlab-credential-crypto.js'; + +export function buildGitLabCredentialAuditAdminQuery(db: WorkerDb, kiloUserId: string) { + return db + .select({ id: kilocode_users.id }) + .from(kilocode_users) + .where( + and( + eq(kilocode_users.id, kiloUserId), + eq(kilocode_users.is_admin, true), + isNull(kilocode_users.blocked_reason) + ) + ) + .limit(1); +} + +export async function runGitLabCredentialAudit( + env: CloudflareEnv, + kiloUserId: string, + input: GitLabCredentialAuditRequest +) { + if (!env.HYPERDRIVE) throw new Error('Hyperdrive not configured'); + const db = getWorkerDb(env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); + const [admin] = await buildGitLabCredentialAuditAdminQuery(db, kiloUserId); + if (!admin) return { authorized: false as const }; + + const crypto = new GitLabCredentialCrypto(env); + const result = await new GitLabCredentialAuditService( + new DrizzleGitLabCredentialAuditStore(db), + crypto + ).audit(input); + return { authorized: true as const, result }; +} diff --git a/services/git-token-service/src/gitlab-credential-audit.test.ts b/services/git-token-service/src/gitlab-credential-audit.test.ts new file mode 100644 index 0000000000..fa4035fe7e --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-audit.test.ts @@ -0,0 +1,273 @@ +import { getWorkerDb } from '@kilocode/db/client'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, +} from '@kilocode/worker-utils/gitlab-credential'; +import { describe, expect, it, vi } from 'vitest'; +import { + GitLabCredentialAuditRequestSchema, + GitLabCredentialAuditService, + buildGitLabAccessTokenCredentialAuditQuery, + buildGitLabOAuthCredentialAuditQuery, +} from './gitlab-credential-audit.js'; + +const timestamp = '2026-07-13T12:00:00.000Z'; + +function oauthCredential(overrides: Record = {}) { + return { + id: 'credential-oauth', + platform_integration_id: 'integration-oauth', + authorized_by_user_id: 'user-1', + provider_subject_id: '42', + provider_subject_login: 'octocat', + provider_base_url: 'https://gitlab.example.com/root', + access_token_encrypted: 'ciphertext-access', + access_token_expires_at: timestamp, + refresh_token_encrypted: 'ciphertext-refresh', + refresh_token_expires_at: null, + oauth_client_secret_encrypted: 'ciphertext-client-secret', + credential_version: 1, + revoked_at: null, + revocation_reason: null, + last_used_at: null, + created_at: timestamp, + updated_at: timestamp, + ...overrides, + }; +} + +function accessTokenCredential(overrides: Record = {}) { + return { + id: 'credential-pat', + platform_integration_id: 'integration-pat', + token_encrypted: 'ciphertext-pat', + expires_at: null, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: 'https://gitlab.example.com/root', + authorized_by_user_id: 'user-1', + provider_metadata: {}, + provider_scopes: null, + provider_verified_at: null, + credential_version: 2, + last_validated_at: null, + last_used_at: null, + created_at: timestamp, + updated_at: timestamp, + ...overrides, + }; +} + +function parent(integrationId: string, integrationType: 'oauth' | 'pat') { + return { + id: integrationId, + platform: 'gitlab', + integration_type: integrationType, + integration_status: 'suspended', + platform_account_id: '42', + platform_account_login: 'octocat', + owned_by_user_id: 'user-1', + owned_by_organization_id: null, + metadata: { gitlab_instance_url: 'https://gitlab.example.com/root' }, + }; +} + +describe('GitLabCredentialAuditService', () => { + it('joins credentials from every GitLab parent without filtering suspended integrations', () => { + const db = getWorkerDb('postgres://query-builder'); + const afterId = '123e4567-e89b-12d3-a456-426614174099'; + const queries = [ + buildGitLabOAuthCredentialAuditQuery(db, afterId).toSQL(), + buildGitLabAccessTokenCredentialAuditQuery(db, afterId).toSQL(), + ]; + + for (const query of queries) { + expect(query.sql).toContain('inner join "platform_integrations"'); + expect(query.sql).toContain('"platform_integrations"."platform" ='); + expect(query.sql).not.toContain('"platform_integrations"."integration_status" ='); + expect(query.params).toContain('gitlab'); + expect(query.params).toContain(afterId); + } + }); + + it('reconstructs every OAuth AAD and returns only key identity, counts, and IDs', async () => { + const auditDecrypt = vi.fn().mockResolvedValue({ + status: 'available' as const, + token: 'decrypted-secret-must-not-be-returned', + }); + const service = new GitLabCredentialAuditService( + { + listCredentials: vi.fn().mockResolvedValue({ + rows: [ + { + table: 'oauth', + parent: parent('integration-oauth', 'oauth'), + credential: oauthCredential(), + }, + ], + nextCursor: null, + }), + }, + { + auditDecrypt, + auditKeyIdentity: vi.fn().mockResolvedValue({ + status: 'available', + keyId: 'active', + publicKeySha256: 'a'.repeat(64), + }), + } + ); + + const result = await service.audit({ limit: 25 }); + + expect(auditDecrypt).toHaveBeenNthCalledWith(1, { + ciphertext: 'ciphertext-access', + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + aad: buildGitLabOAuthCredentialAad({ + credentialId: 'credential-oauth', + integrationId: 'integration-oauth', + providerBaseUrl: 'https://gitlab.example.com/root', + owner: { type: 'user', id: 'user-1' }, + authorizedByUserId: 'user-1', + credentialVersion: 1, + kind: 'access', + }), + }); + expect(auditDecrypt).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ aad: expect.stringContaining('"kind":"refresh"') }) + ); + expect(auditDecrypt).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ aad: expect.stringContaining('"kind":"oauth-client-secret"') }) + ); + expect(result).toEqual({ + activeKey: { keyId: 'active', publicKeySha256: 'a'.repeat(64) }, + counts: { + credentials: 1, + secrets: 3, + passedCredentials: 1, + profileFailures: 0, + configurationFailures: 0, + parseFailures: 0, + unknownKeyFailures: 0, + decryptOrAadFailures: 0, + }, + failingCredentials: { + profile: [], + configuration: [], + parse: [], + unknownKey: [], + decryptOrAad: [], + }, + nextCursor: null, + }); + expect(JSON.stringify(result)).not.toMatch(/decrypted-secret|ciphertext-/); + }); + + it('reconstructs PAT AAD from the validated parent owner and version', async () => { + const auditDecrypt = vi.fn().mockResolvedValue({ status: 'available', token: 'pat-secret' }); + const service = new GitLabCredentialAuditService( + { + listCredentials: vi.fn().mockResolvedValue({ + rows: [ + { + table: 'access-token', + parent: parent('integration-pat', 'pat'), + credential: accessTokenCredential(), + }, + ], + nextCursor: null, + }), + }, + { + auditDecrypt, + auditKeyIdentity: vi.fn().mockResolvedValue({ + status: 'available', + keyId: 'active', + publicKeySha256: 'b'.repeat(64), + }), + } + ); + + await service.audit({ limit: 1 }); + + expect(auditDecrypt).toHaveBeenCalledWith({ + ciphertext: 'ciphertext-pat', + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: buildGitLabPersonalAccessTokenAad({ + credentialId: 'credential-pat', + integrationId: 'integration-pat', + providerBaseUrl: 'https://gitlab.example.com/root', + owner: { type: 'user', id: 'user-1' }, + authorizedByUserId: 'user-1', + credentialVersion: 2, + }), + }); + }); + + it('classifies profile and decrypt failures by integration and credential ID only', async () => { + const service = new GitLabCredentialAuditService( + { + listCredentials: vi.fn().mockResolvedValue({ + rows: [ + { + table: 'oauth', + parent: parent('integration-profile', 'oauth'), + credential: oauthCredential({ + id: 'credential-profile', + platform_integration_id: 'integration-profile', + provider_subject_id: 'wrong-subject', + }), + }, + { + table: 'access-token', + parent: parent('integration-decrypt', 'pat'), + credential: accessTokenCredential({ + id: 'credential-decrypt', + platform_integration_id: 'integration-decrypt', + }), + }, + ], + nextCursor: null, + }), + }, + { + auditDecrypt: vi.fn().mockResolvedValue({ status: 'decrypt_failed' }), + auditKeyIdentity: vi.fn().mockResolvedValue({ + status: 'available', + keyId: 'active', + publicKeySha256: 'c'.repeat(64), + }), + } + ); + + const result = await service.audit({ limit: 2 }); + + expect(result.failingCredentials.profile).toEqual([ + { integrationId: 'integration-profile', credentialId: 'credential-profile' }, + ]); + expect(result.failingCredentials.decryptOrAad).toEqual([ + { integrationId: 'integration-decrypt', credentialId: 'credential-decrypt' }, + ]); + }); + + it('strictly bounds cursor and limit and rejects removed legacy-comparison input', () => { + expect(GitLabCredentialAuditRequestSchema.parse({})).toEqual({ limit: 100 }); + expect( + GitLabCredentialAuditRequestSchema.safeParse({ + cursor: 'access-token:123e4567-e89b-12d3-a456-426614174099', + limit: 1, + }).success + ).toBe(true); + expect(GitLabCredentialAuditRequestSchema.safeParse({ cursor: 'access-token' }).success).toBe( + true + ); + expect(GitLabCredentialAuditRequestSchema.safeParse({ limit: 101 }).success).toBe(false); + expect(GitLabCredentialAuditRequestSchema.safeParse({ compareLegacy: true }).success).toBe( + false + ); + }); +}); diff --git a/services/git-token-service/src/gitlab-credential-audit.ts b/services/git-token-service/src/gitlab-credential-audit.ts new file mode 100644 index 0000000000..95cb74cf77 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-audit.ts @@ -0,0 +1,403 @@ +import type { WorkerDb } from '@kilocode/db/client'; +import { + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + GitLabOAuthCredentialRowSchema, + GitLabPersonalAccessTokenCredentialRowSchema, + GitLabProjectAccessTokenCredentialRowSchema, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, + type GitLabCredentialOwner, +} from '@kilocode/worker-utils/gitlab-credential'; +import { and, asc, eq, gt } from 'drizzle-orm'; +import { z } from 'zod'; +import { DEFAULT_GITLAB_INSTANCE_URL } from './gitlab-constants.js'; +import type { GitLabCredentialCrypto } from './gitlab-credential-crypto.js'; +import { normalizeGitLabInstanceUrl } from './gitlab-url.js'; + +const AuditCursorSchema = z.string().refine(value => parseAuditCursor(value) !== null, { + message: 'Invalid GitLab credential audit cursor', +}); + +export const GitLabCredentialAuditRequestSchema = z + .object({ + cursor: AuditCursorSchema.optional(), + limit: z.number().int().min(1).max(100).default(100), + }) + .strict(); + +export type GitLabCredentialAuditRequest = z.infer; + +type AuditTable = 'oauth' | 'access-token'; +type AuditCursor = { table: AuditTable; afterId?: string }; + +type GitLabCredentialAuditParent = { + id: string; + platform: string; + integration_type: string; + integration_status: string | null; + platform_account_id: string | null; + platform_account_login: string | null; + owned_by_user_id: string | null; + owned_by_organization_id: string | null; + metadata: unknown; +}; + +export type GitLabCredentialAuditRow = { + table: AuditTable; + parent: GitLabCredentialAuditParent; + credential: { id: string } & Record; +}; + +export type GitLabCredentialAuditPage = { + rows: GitLabCredentialAuditRow[]; + nextCursor: string | null; +}; + +export type GitLabCredentialAuditStore = { + listCredentials(input: { cursor?: string; limit: number }): Promise; +}; + +type AuditId = { integrationId: string; credentialId: string }; +type FailureKind = 'profile' | 'configuration' | 'parse' | 'unknownKey' | 'decryptOrAad'; + +type AuditSecret = { + ciphertext: string; + scheme: + | typeof GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME + | typeof GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME + | typeof GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME; + aad: string; +}; + +function parseAuditCursor(value: string): AuditCursor | null { + if (value === 'access-token') return { table: 'access-token' }; + const match = /^(oauth|access-token):(.+)$/.exec(value); + if (!match || !z.uuid().safeParse(match[2]).success) return null; + return { table: match[1] as AuditTable, afterId: match[2] }; +} + +function encodeAuditCursor(cursor: AuditCursor): string { + return cursor.afterId ? `${cursor.table}:${cursor.afterId}` : cursor.table; +} + +function lastCredentialId(rows: Array<{ credential: { id: string } }>): string { + const last = rows.at(-1); + if (!last) throw new Error('Cannot paginate an empty credential batch'); + return last.credential.id; +} + +export function buildGitLabOAuthCredentialAuditQuery(db: WorkerDb, afterId?: string) { + return db + .select({ parent: platform_integrations, credential: platform_oauth_credentials }) + .from(platform_oauth_credentials) + .innerJoin( + platform_integrations, + eq(platform_integrations.id, platform_oauth_credentials.platform_integration_id) + ) + .where( + and( + eq(platform_integrations.platform, 'gitlab'), + ...(afterId ? [gt(platform_oauth_credentials.id, afterId)] : []) + ) + ) + .orderBy(asc(platform_oauth_credentials.id)); +} + +export function buildGitLabAccessTokenCredentialAuditQuery(db: WorkerDb, afterId?: string) { + return db + .select({ parent: platform_integrations, credential: platform_access_token_credentials }) + .from(platform_access_token_credentials) + .innerJoin( + platform_integrations, + eq(platform_integrations.id, platform_access_token_credentials.platform_integration_id) + ) + .where( + and( + eq(platform_integrations.platform, 'gitlab'), + ...(afterId ? [gt(platform_access_token_credentials.id, afterId)] : []) + ) + ) + .orderBy(asc(platform_access_token_credentials.id)); +} + +export class DrizzleGitLabCredentialAuditStore implements GitLabCredentialAuditStore { + constructor(private db: WorkerDb) {} + + async listCredentials(input: { + cursor?: string; + limit: number; + }): Promise { + const cursor = input.cursor ? parseAuditCursor(input.cursor) : { table: 'oauth' as const }; + if (!cursor) throw new Error('Invalid GitLab credential audit cursor'); + + const rows: GitLabCredentialAuditRow[] = []; + let remaining = input.limit; + if (cursor.table === 'oauth') { + const oauthRows = await buildGitLabOAuthCredentialAuditQuery(this.db, cursor.afterId).limit( + remaining + 1 + ); + if (oauthRows.length > remaining) { + const selected = oauthRows.slice(0, remaining); + return { + rows: selected.map(row => ({ table: 'oauth', ...row })), + nextCursor: encodeAuditCursor({ + table: 'oauth', + afterId: lastCredentialId(selected), + }), + }; + } + rows.push(...oauthRows.map(row => ({ table: 'oauth' as const, ...row }))); + remaining -= oauthRows.length; + if (remaining === 0) return { rows, nextCursor: 'access-token' }; + } + + const accessTokenRows = await buildGitLabAccessTokenCredentialAuditQuery( + this.db, + cursor.table === 'access-token' ? cursor.afterId : undefined + ).limit(remaining + 1); + if (accessTokenRows.length > remaining) { + const selected = accessTokenRows.slice(0, remaining); + rows.push(...selected.map(row => ({ table: 'access-token' as const, ...row }))); + return { + rows, + nextCursor: encodeAuditCursor({ + table: 'access-token', + afterId: lastCredentialId(selected), + }), + }; + } + + rows.push(...accessTokenRows.map(row => ({ table: 'access-token' as const, ...row }))); + return { rows, nextCursor: null }; + } +} + +function parentOwner(parent: GitLabCredentialAuditParent): GitLabCredentialOwner | null { + if (parent.owned_by_user_id && parent.owned_by_organization_id === null) { + return { type: 'user', id: parent.owned_by_user_id }; + } + if (parent.owned_by_organization_id && parent.owned_by_user_id === null) { + return { type: 'org', id: parent.owned_by_organization_id }; + } + return null; +} + +function metadataRecord(value: unknown): Record | null { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : null; +} + +function parentBaseUrl(parent: GitLabCredentialAuditParent): string | null { + const metadata = metadataRecord(parent.metadata); + if (!metadata) return null; + const value = metadata.gitlab_instance_url; + if (value !== undefined && typeof value !== 'string') return null; + return normalizeGitLabInstanceUrl(value ?? DEFAULT_GITLAB_INSTANCE_URL); +} + +function oauthAuditSecrets(row: GitLabCredentialAuditRow): AuditSecret[] | null { + const credential = GitLabOAuthCredentialRowSchema.safeParse(row.credential); + const owner = parentOwner(row.parent); + const providerBaseUrl = parentBaseUrl(row.parent); + if ( + !credential.success || + !owner || + !providerBaseUrl || + row.parent.id !== credential.data.platform_integration_id || + row.parent.platform !== 'gitlab' || + row.parent.integration_type !== 'oauth' || + credential.data.provider_base_url !== providerBaseUrl || + credential.data.provider_subject_id !== row.parent.platform_account_id || + credential.data.provider_subject_login !== row.parent.platform_account_login || + (owner.type === 'user' && credential.data.authorized_by_user_id !== owner.id) + ) { + return null; + } + + const buildSecret = ( + ciphertext: string, + kind: 'access' | 'refresh' | 'oauth-client-secret' + ): AuditSecret => ({ + ciphertext, + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + aad: buildGitLabOAuthCredentialAad({ + credentialId: credential.data.id, + integrationId: credential.data.platform_integration_id, + providerBaseUrl: credential.data.provider_base_url, + owner, + authorizedByUserId: credential.data.authorized_by_user_id, + credentialVersion: credential.data.credential_version, + kind, + }), + }); + const secrets = [buildSecret(credential.data.access_token_encrypted, 'access')]; + if (credential.data.refresh_token_encrypted) { + secrets.push(buildSecret(credential.data.refresh_token_encrypted, 'refresh')); + } + if (credential.data.oauth_client_secret_encrypted) { + secrets.push(buildSecret(credential.data.oauth_client_secret_encrypted, 'oauth-client-secret')); + } + return secrets; +} + +function accessTokenAuditSecrets(row: GitLabCredentialAuditRow): AuditSecret[] | null { + const owner = parentOwner(row.parent); + const providerBaseUrl = parentBaseUrl(row.parent); + if (!owner || !providerBaseUrl || row.parent.platform !== 'gitlab') return null; + + const projectCredential = GitLabProjectAccessTokenCredentialRowSchema.safeParse(row.credential); + if (projectCredential.success) { + const credential = projectCredential.data; + if ( + row.parent.id !== credential.platform_integration_id || + credential.provider_base_url !== providerBaseUrl + ) { + return null; + } + return [ + { + ciphertext: credential.token_encrypted, + scheme: GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: buildGitLabProjectAccessTokenAad({ + credentialId: credential.id, + integrationId: credential.platform_integration_id, + providerBaseUrl: credential.provider_base_url, + owner, + providerResourceId: credential.provider_resource_id, + credentialVersion: credential.credential_version, + }), + }, + ]; + } + + const personalCredential = GitLabPersonalAccessTokenCredentialRowSchema.safeParse(row.credential); + if (!personalCredential.success) return null; + const credential = personalCredential.data; + if ( + row.parent.id !== credential.platform_integration_id || + row.parent.integration_type !== 'pat' || + credential.provider_base_url !== providerBaseUrl || + (owner.type === 'user' && credential.authorized_by_user_id !== owner.id) + ) { + return null; + } + return [ + { + ciphertext: credential.token_encrypted, + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: buildGitLabPersonalAccessTokenAad({ + credentialId: credential.id, + integrationId: credential.platform_integration_id, + providerBaseUrl: credential.provider_base_url, + owner, + authorizedByUserId: credential.authorized_by_user_id, + credentialVersion: credential.credential_version, + }), + }, + ]; +} + +function auditSecretsForRow(row: GitLabCredentialAuditRow): AuditSecret[] | null { + return row.table === 'oauth' ? oauthAuditSecrets(row) : accessTokenAuditSecrets(row); +} + +export class GitLabCredentialAuditService { + constructor( + private store: GitLabCredentialAuditStore, + private crypto: Pick + ) {} + + async audit(input: GitLabCredentialAuditRequest) { + let activeKey: { keyId: string; publicKeySha256: string } | null = null; + try { + const key = await this.crypto.auditKeyIdentity(); + if (key.status === 'available') { + activeKey = { keyId: key.keyId, publicKeySha256: key.publicKeySha256 }; + } + } catch { + // Each credential is still classified as a configuration failure below. + } + const page = await this.store.listCredentials({ cursor: input.cursor, limit: input.limit }); + const failingCredentials: Record = { + profile: [], + configuration: [], + parse: [], + unknownKey: [], + decryptOrAad: [], + }; + let secrets = 0; + let passedCredentials = 0; + + for (const row of page.rows) { + const id = { integrationId: row.parent.id, credentialId: row.credential.id }; + const auditSecrets = auditSecretsForRow(row); + if (!auditSecrets) { + failingCredentials.profile.push(id); + continue; + } + + const failures = new Set(); + for (const secret of auditSecrets) { + secrets += 1; + let result: Awaited>; + try { + result = await this.crypto.auditDecrypt({ + ciphertext: secret.ciphertext, + scheme: secret.scheme, + aad: secret.aad, + }); + } catch { + failures.add('decryptOrAad'); + continue; + } + switch (result.status) { + case 'configuration_error': + failures.add('configuration'); + break; + case 'invalid_envelope': + failures.add('parse'); + break; + case 'unknown_key': + failures.add('unknownKey'); + break; + case 'decrypt_failed': + failures.add('decryptOrAad'); + break; + case 'available': + break; + } + } + + if (failures.size === 0) { + passedCredentials += 1; + } else { + for (const failure of failures) failingCredentials[failure].push(id); + } + } + + return { + activeKey, + counts: { + credentials: page.rows.length, + secrets, + passedCredentials, + profileFailures: failingCredentials.profile.length, + configurationFailures: failingCredentials.configuration.length, + parseFailures: failingCredentials.parse.length, + unknownKeyFailures: failingCredentials.unknownKey.length, + decryptOrAadFailures: failingCredentials.decryptOrAad.length, + }, + failingCredentials, + nextCursor: page.nextCursor, + }; + } +} diff --git a/services/git-token-service/src/gitlab-credential-broker-handler.test.ts b/services/git-token-service/src/gitlab-credential-broker-handler.test.ts new file mode 100644 index 0000000000..485df7075b --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-broker-handler.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { GitLabCredentialBrokerRequestSchema } from './gitlab-credential-broker-handler.js'; + +describe('GitLabCredentialBrokerRequestSchema', () => { + it.each([ + { credential: 'integration', integrationId: '2d397344-f0e9-4f5d-a822-e005209d1c88' }, + { + credential: 'project-exact', + integrationId: '2d397344-f0e9-4f5d-a822-e005209d1c88', + projectId: '42', + }, + ])('accepts the strict $credential selector', selector => { + expect(GitLabCredentialBrokerRequestSchema.safeParse(selector).success).toBe(true); + }); + + it('rejects actor claims and fallback-shaped fields from request JSON', () => { + expect( + GitLabCredentialBrokerRequestSchema.safeParse({ + credential: 'integration', + integrationId: '2d397344-f0e9-4f5d-a822-e005209d1c88', + userId: 'attacker', + projectId: '42', + }).success + ).toBe(false); + }); +}); diff --git a/services/git-token-service/src/gitlab-credential-broker-handler.ts b/services/git-token-service/src/gitlab-credential-broker-handler.ts new file mode 100644 index 0000000000..1db27d5016 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-broker-handler.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; +import { GitLabCredentialBroker } from './gitlab-credential-broker.js'; +import { GitLabCredentialCrypto } from './gitlab-credential-crypto.js'; +import { + GitLabCredentialService, + type GitLabCredentialActor, +} from './gitlab-credential-service.js'; +import { DrizzleGitLabCredentialStore } from './gitlab-credential-store.js'; +import { GitLabLookupService } from './gitlab-lookup-service.js'; +import { GitLabOAuthCredentialRefresher } from './gitlab-oauth-credential-refresher.js'; +import { GitLabTokenService } from './gitlab-token-service.js'; + +export const GitLabCredentialBrokerRequestSchema = z.discriminatedUnion('credential', [ + z + .object({ + credential: z.literal('integration'), + integrationId: z.uuid(), + }) + .strict(), + z + .object({ + credential: z.literal('project-exact'), + integrationId: z.uuid(), + projectId: z.string().regex(/^[1-9][0-9]*$/), + }) + .strict(), +]); + +export type GitLabCredentialBrokerRequest = z.infer; + +export function createGitLabCredentialService(env: CloudflareEnv): GitLabCredentialService { + const crypto = new GitLabCredentialCrypto(env); + const refresher = new GitLabOAuthCredentialRefresher(env, { crypto }); + return new GitLabCredentialService(new DrizzleGitLabCredentialStore(env), crypto, refresher); +} + +export function createGitLabCredentialBroker(env: CloudflareEnv): GitLabCredentialBroker { + const lookupService = new GitLabLookupService(env); + const tokenService = new GitLabTokenService(env, new GitLabOAuthCredentialRefresher(env)); + const credentialService = createGitLabCredentialService(env); + return new GitLabCredentialBroker({ + findIntegration: (actor, integrationId) => + lookupService.findGitLabIntegration(actor, integrationId), + getLegacyIntegrationToken: (actor, integrationId, metadata) => + tokenService.getToken(integrationId, metadata, actor), + getEncryptedCredential: (actor, selector) => credentialService.getCredential(actor, selector), + hasProjectCredentialCandidates: (actor, integrationId) => + credentialService.hasProjectCredentialCandidates(actor, integrationId), + reportFallback: event => { + console.warn(JSON.stringify({ message: 'GitLab legacy credential fallback', ...event })); + }, + }); +} + +export async function handleGitLabCredentialBrokerRequest( + env: CloudflareEnv, + actor: GitLabCredentialActor, + selector: GitLabCredentialBrokerRequest +) { + const result = await createGitLabCredentialBroker(env).resolveCredential(actor, selector); + if (result.status !== 'available') return result; + return { + status: 'available', + token: result.token, + instanceUrl: result.instanceUrl, + glabIsOAuth2: result.glabIsOAuth2, + } as const; +} diff --git a/services/git-token-service/src/gitlab-credential-broker.test.ts b/services/git-token-service/src/gitlab-credential-broker.test.ts new file mode 100644 index 0000000000..2dc5ef0ec2 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-broker.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it, vi } from 'vitest'; +import { GitLabCredentialBroker } from './gitlab-credential-broker.js'; + +describe('GitLabCredentialBroker', () => { + it('returns an encrypted credential without reading legacy plaintext', async () => { + const findIntegration = vi.fn(); + const broker = new GitLabCredentialBroker({ + findIntegration, + getLegacyIntegrationToken: vi.fn(), + getEncryptedCredential: async () => ({ + status: 'available', + token: 'encrypted-secret', + instanceUrl: 'https://gitlab.example.com', + integrationId: 'integration-1', + glabIsOAuth2: true, + credentialId: 'credential-1', + credentialVersion: 1, + source: { type: 'integration' }, + }), + hasProjectCredentialCandidates: vi.fn().mockResolvedValue(false), + reportFallback: vi.fn(), + }); + + await expect( + broker.resolveCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: 'integration-1' } + ) + ).resolves.toEqual({ + status: 'available', + token: 'encrypted-secret', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: true, + integrationId: 'integration-1', + credentialId: 'credential-1', + credentialVersion: 1, + source: { type: 'integration' }, + }); + expect(findIntegration).not.toHaveBeenCalled(); + }); + + it('returns the matching legacy integration token only when the encrypted row is absent', async () => { + const reportFallback = vi.fn(); + const broker = new GitLabCredentialBroker({ + findIntegration: async () => ({ + success: true, + integrationId: 'integration-1', + integrationType: 'pat', + accountId: '123', + accountLogin: 'octocat', + metadata: { + access_token: 'legacy-secret', + auth_type: 'pat', + gitlab_instance_url: 'https://gitlab.example.com', + }, + }), + getLegacyIntegrationToken: async () => ({ + success: true, + token: 'legacy-secret', + instanceUrl: 'https://gitlab.example.com', + }), + getEncryptedCredential: async () => ({ status: 'credential_absent' }), + hasProjectCredentialCandidates: vi.fn().mockResolvedValue(false), + reportFallback, + }); + + await expect( + broker.resolveCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: 'integration-1' } + ) + ).resolves.toEqual({ + status: 'available', + token: 'legacy-secret', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: false, + integrationId: 'integration-1', + source: { type: 'integration' }, + }); + expect(reportFallback).toHaveBeenCalledWith({ + integrationId: 'integration-1', + credential: 'integration', + status: 'resolved', + }); + expect(JSON.stringify(reportFallback.mock.calls)).not.toContain('legacy-secret'); + }); + + it('fails closed without reading plaintext when an encrypted row is invalid', async () => { + const findIntegration = vi.fn(); + const getLegacyIntegrationToken = vi.fn(); + const broker = new GitLabCredentialBroker({ + findIntegration, + getLegacyIntegrationToken, + getEncryptedCredential: async () => ({ status: 'reconnect_required' }), + hasProjectCredentialCandidates: vi.fn().mockResolvedValue(false), + reportFallback: vi.fn(), + }); + + await expect( + broker.resolveCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: 'integration-1' } + ) + ).resolves.toEqual({ status: 'reconnect_required' }); + expect(findIntegration).not.toHaveBeenCalled(); + expect(getLegacyIntegrationToken).not.toHaveBeenCalled(); + }); + + it('falls back to only the exact legacy project token when its encrypted row is absent', async () => { + const getLegacyIntegrationToken = vi.fn(); + const broker = new GitLabCredentialBroker({ + findIntegration: async () => ({ + success: true, + integrationId: 'integration-1', + integrationType: 'oauth', + accountId: '123', + accountLogin: 'octocat', + metadata: { + access_token: 'integration-secret', + auth_type: 'oauth', + gitlab_instance_url: 'https://gitlab.example.com', + project_tokens: { '42': { token: 'legacy-project-secret' } }, + }, + }), + getLegacyIntegrationToken, + getEncryptedCredential: async () => ({ status: 'credential_absent' }), + hasProjectCredentialCandidates: vi.fn().mockResolvedValue(false), + reportFallback: vi.fn(), + }); + + await expect( + broker.resolveCredential( + { userId: 'user-1' }, + { credential: 'project-exact', integrationId: 'integration-1', projectId: '42' } + ) + ).resolves.toEqual({ + status: 'available', + token: 'legacy-project-secret', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: false, + integrationId: 'integration-1', + source: { type: 'project', projectId: '42' }, + }); + expect(getLegacyIntegrationToken).not.toHaveBeenCalled(); + }); +}); diff --git a/services/git-token-service/src/gitlab-credential-broker.ts b/services/git-token-service/src/gitlab-credential-broker.ts new file mode 100644 index 0000000000..34e0d72f3d --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-broker.ts @@ -0,0 +1,159 @@ +import type { + GitLabCredentialActor, + GitLabCredentialResult, + GitLabCredentialSelector, +} from './gitlab-credential-service.js'; +import type { GitLabIntegrationMetadata, GitLabLookupResult } from './gitlab-lookup-service.js'; +import { DEFAULT_GITLAB_INSTANCE_URL } from './gitlab-constants.js'; +import type { GitLabTokenResult } from './gitlab-token-service.js'; +import { normalizeGitLabInstanceUrl } from './gitlab-url.js'; + +export type GitLabCredentialBrokerResult = + | { + status: 'available'; + token: string; + instanceUrl: string; + glabIsOAuth2: boolean; + integrationId: string; + credentialId?: string; + credentialVersion?: number; + source: { type: 'integration' } | { type: 'project'; projectId: string }; + } + | { status: 'invalid_request' } + | { status: 'not_connected' } + | { status: 'reconnect_required' } + | { status: 'temporarily_unavailable' }; + +type GitLabCredentialBrokerDependencies = { + findIntegration(actor: GitLabCredentialActor, integrationId: string): Promise; + getLegacyIntegrationToken( + actor: GitLabCredentialActor, + integrationId: string, + metadata: GitLabIntegrationMetadata + ): Promise; + getEncryptedCredential( + actor: GitLabCredentialActor, + selector: GitLabCredentialSelector + ): Promise; + hasProjectCredentialCandidates( + actor: GitLabCredentialActor, + integrationId: string + ): Promise; + reportFallback(event: { + integrationId: string; + credential: GitLabCredentialSelector['credential']; + status: string; + }): void; +}; + +function mapLookupFailure(result: Exclude) { + switch (result.reason) { + case 'database_not_configured': + return { status: 'temporarily_unavailable' } as const; + case 'invalid_org_id': + return { status: 'invalid_request' } as const; + case 'no_integration_found': + return { status: 'not_connected' } as const; + } +} + +function mapLegacyFailure(result: Exclude) { + return result.reason === 'token_refresh_failed' + ? ({ status: 'temporarily_unavailable' } as const) + : ({ status: 'reconnect_required' } as const); +} + +export class GitLabCredentialBroker { + constructor(private dependencies: GitLabCredentialBrokerDependencies) {} + + hasProjectCredentialCandidates( + actor: GitLabCredentialActor, + integrationId: string + ): Promise { + return this.dependencies.hasProjectCredentialCandidates(actor, integrationId); + } + + async resolveCredential( + actor: GitLabCredentialActor, + selector: GitLabCredentialSelector + ): Promise { + const encrypted = await this.dependencies.getEncryptedCredential(actor, selector); + if (encrypted.status === 'available') { + return { + status: 'available', + token: encrypted.token, + instanceUrl: encrypted.instanceUrl, + glabIsOAuth2: encrypted.glabIsOAuth2, + integrationId: encrypted.integrationId, + credentialId: encrypted.credentialId, + credentialVersion: encrypted.credentialVersion, + source: encrypted.source, + }; + } + if (encrypted.status !== 'credential_absent') return encrypted; + + const integration = await this.dependencies.findIntegration(actor, selector.integrationId); + if (!integration.success) return mapLookupFailure(integration); + if (selector.credential === 'project-exact') { + const token = integration.metadata.project_tokens?.[selector.projectId]?.token; + const instanceUrl = normalizeGitLabInstanceUrl( + integration.metadata.gitlab_instance_url ?? DEFAULT_GITLAB_INSTANCE_URL + ); + if (!token || !instanceUrl) return { status: 'reconnect_required' }; + this.dependencies.reportFallback({ + integrationId: integration.integrationId, + credential: selector.credential, + status: 'resolved', + }); + return { + status: 'available', + token, + instanceUrl, + glabIsOAuth2: false, + integrationId: integration.integrationId, + source: { type: 'project', projectId: selector.projectId }, + }; + } + + const legacy = await this.dependencies.getLegacyIntegrationToken( + actor, + integration.integrationId, + integration.metadata + ); + if (!legacy.success && legacy.reason === 'encrypted_credential_available') { + const winner = await this.dependencies.getEncryptedCredential(actor, selector); + if (winner.status !== 'available') { + return winner.status === 'credential_absent' + ? { status: 'temporarily_unavailable' } + : winner; + } + return { + status: 'available', + token: winner.token, + instanceUrl: winner.instanceUrl, + glabIsOAuth2: winner.glabIsOAuth2, + integrationId: winner.integrationId, + credentialId: winner.credentialId, + credentialVersion: winner.credentialVersion, + source: winner.source, + }; + } + if (!legacy.success) return mapLegacyFailure(legacy); + this.dependencies.reportFallback({ + integrationId: integration.integrationId, + credential: selector.credential, + status: 'resolved', + }); + return { + status: 'available', + token: legacy.token, + instanceUrl: legacy.instanceUrl, + glabIsOAuth2: + integration.integrationType === 'oauth' || integration.integrationType === 'pat' + ? integration.integrationType === 'oauth' + : integration.metadata.auth_type === 'oauth', + integrationId: integration.integrationId, + source: { type: 'integration' }, + }; + } +} diff --git a/services/git-token-service/src/gitlab-credential-crypto.test.ts b/services/git-token-service/src/gitlab-credential-crypto.test.ts new file mode 100644 index 0000000000..045ab68764 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-crypto.test.ts @@ -0,0 +1,140 @@ +import { createHash, createPublicKey, generateKeyPairSync } from 'node:crypto'; +import { encryptKeyedEnvelope } from '@kilocode/encryption'; +import { + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildGitLabPersonalAccessTokenAad, +} from '@kilocode/worker-utils/gitlab-credential'; +import { describe, expect, it } from 'vitest'; +import { GitLabCredentialCrypto } from './gitlab-credential-crypto.js'; + +function keyConfiguration() { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + return { + keyId: 'active', + publicKey, + privateKey, + env: { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: 'active', + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: Buffer.from(publicKey).toString('base64'), + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: Buffer.from(privateKey).toString('base64'), + }, + }; +} + +describe('GitLabCredentialCrypto', () => { + it('returns only the active key ID and derived public-key fingerprint for audit preflight', async () => { + const keys = keyConfiguration(); + const expectedFingerprint = createHash('sha256') + .update(createPublicKey(keys.publicKey).export({ type: 'spki', format: 'der' })) + .digest('hex'); + + const result = await new GitLabCredentialCrypto(keys.env).auditKeyIdentity(); + + expect(result).toEqual({ + status: 'available', + keyId: 'active', + publicKeySha256: expectedFingerprint, + }); + expect(JSON.stringify(result)).not.toContain('BEGIN'); + expect(JSON.stringify(result)).not.toContain(Buffer.from(keys.privateKey).toString('base64')); + }); + + it('does not expose an identity when public and private audit keys do not match', async () => { + const keys = keyConfiguration(); + const differentKeys = keyConfiguration(); + + await expect( + new GitLabCredentialCrypto({ + ...keys.env, + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: + differentKeys.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY, + }).auditKeyIdentity() + ).resolves.toEqual({ status: 'configuration_error' }); + }); + + it('decrypts a GitLab PAT with the deployed platform credential keypair', async () => { + const keys = keyConfiguration(); + const aad = buildGitLabPersonalAccessTokenAad({ + credentialId: 'credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-1' }, + authorizedByUserId: 'user-1', + credentialVersion: 1, + }); + const ciphertext = encryptKeyedEnvelope( + 'glpat-secret', + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + { keyId: keys.keyId, publicKeyPem: keys.publicKey }, + aad + ); + + await expect( + new GitLabCredentialCrypto(keys.env).decrypt({ + ciphertext, + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad, + }) + ).resolves.toEqual({ status: 'available', token: 'glpat-secret' }); + }); + + it('encrypts a refreshed GitLab token for the active platform credential key', async () => { + const keys = keyConfiguration(); + const aad = buildGitLabPersonalAccessTokenAad({ + credentialId: 'credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-1' }, + authorizedByUserId: 'user-1', + credentialVersion: 2, + }); + const crypto = new GitLabCredentialCrypto(keys.env); + + const encrypted = await crypto.encrypt({ + plaintext: 'refreshed-secret', + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad, + }); + expect(encrypted.status).toBe('available'); + if (encrypted.status !== 'available') throw new Error('expected encrypted credential'); + await expect( + crypto.decrypt({ + ciphertext: encrypted.ciphertext, + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad, + }) + ).resolves.toEqual({ status: 'available', token: 'refreshed-secret' }); + }); + + it('classifies audit failures without returning persisted ciphertext', async () => { + const keys = keyConfiguration(); + const crypto = new GitLabCredentialCrypto(keys.env); + + await expect( + crypto.auditDecrypt({ + ciphertext: '{', + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: 'expected-aad', + }) + ).resolves.toEqual({ status: 'invalid_envelope' }); + + const encrypted = await crypto.encrypt({ + plaintext: 'secret-value', + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: 'original-aad', + }); + expect(encrypted.status).toBe('available'); + if (encrypted.status !== 'available') throw new Error('expected encrypted credential'); + await expect( + crypto.auditDecrypt({ + ciphertext: encrypted.ciphertext, + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: 'different-aad', + }) + ).resolves.toEqual({ status: 'decrypt_failed' }); + }); +}); diff --git a/services/git-token-service/src/gitlab-credential-crypto.ts b/services/git-token-service/src/gitlab-credential-crypto.ts new file mode 100644 index 0000000000..0732ea5911 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-crypto.ts @@ -0,0 +1,194 @@ +import { createHash, createPrivateKey, createPublicKey } from 'node:crypto'; +import { + decryptKeyedEnvelope, + encryptKeyedEnvelope, + parseKeyedEnvelope, +} from '@kilocode/encryption'; +import type { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, +} from '@kilocode/worker-utils/gitlab-credential'; + +type Secret = SecretsStoreSecret | string | undefined; + +export type GitLabCredentialCryptoEnv = { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID?: Secret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY?: Secret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY?: Secret; +}; + +type GitLabCredentialEnvelopeScheme = + | typeof GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME + | typeof GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME + | typeof GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME; + +export type GitLabCredentialDecryptionResult = + | { status: 'available'; token: string } + | { status: 'temporarily_unavailable' } + | { status: 'unreadable' }; + +export type GitLabCredentialAuditDecryptionResult = + | { status: 'available'; token: string } + | { status: 'configuration_error' } + | { status: 'invalid_envelope' } + | { status: 'unknown_key' } + | { status: 'decrypt_failed' }; + +async function resolveSecret(secret: Secret): Promise { + if (!secret) return null; + const value = typeof secret === 'string' ? secret : await secret.get(); + return value || null; +} + +export class GitLabCredentialCrypto { + constructor(private env: GitLabCredentialCryptoEnv) {} + + async auditKeyIdentity(): Promise< + | { status: 'available'; keyId: string; publicKeySha256: string } + | { status: 'configuration_error' } + > { + const key = await this.loadActiveKey(); + if (key.status !== 'available') return { status: 'configuration_error' }; + return { + status: 'available', + keyId: key.keyId, + publicKeySha256: key.publicKeySha256, + }; + } + + async encrypt(input: { + plaintext: string; + scheme: GitLabCredentialEnvelopeScheme; + aad: string; + }): Promise<{ status: 'available'; ciphertext: string } | { status: 'temporarily_unavailable' }> { + const key = await this.loadActiveKey(); + if (key.status !== 'available') return key; + try { + return { + status: 'available', + ciphertext: encryptKeyedEnvelope( + input.plaintext, + input.scheme, + { keyId: key.keyId, publicKeyPem: key.publicKeyPem }, + input.aad + ), + }; + } catch { + return { status: 'temporarily_unavailable' }; + } + } + + async decrypt(input: { + ciphertext: string; + scheme: GitLabCredentialEnvelopeScheme; + aad: string; + }): Promise { + const key = await this.loadActiveKey(); + if (key.status !== 'available') return key; + + try { + const envelope = parseKeyedEnvelope(input.ciphertext, input.scheme); + if (envelope.keyId !== key.keyId) return { status: 'unreadable' }; + return { + status: 'available', + token: decryptKeyedEnvelope( + input.ciphertext, + input.scheme, + { active: { keyId: key.keyId, privateKeyPem: key.privateKeyPem } }, + input.aad + ), + }; + } catch { + return { status: 'unreadable' }; + } + } + + async auditDecrypt(input: { + ciphertext: string; + scheme: GitLabCredentialEnvelopeScheme; + aad: string; + }): Promise { + const key = await this.loadActiveKey(); + if (key.status !== 'available') return { status: 'configuration_error' }; + + let envelope: ReturnType; + try { + envelope = parseKeyedEnvelope(input.ciphertext, input.scheme); + } catch { + return { status: 'invalid_envelope' }; + } + if (envelope.keyId !== key.keyId) return { status: 'unknown_key' }; + + try { + return { + status: 'available', + token: decryptKeyedEnvelope( + input.ciphertext, + input.scheme, + { active: { keyId: key.keyId, privateKeyPem: key.privateKeyPem } }, + input.aad + ), + }; + } catch { + return { status: 'decrypt_failed' }; + } + } + + private async loadActiveKey(): Promise< + | { + status: 'available'; + keyId: string; + publicKeyPem: string; + privateKeyPem: string; + publicKeySha256: string; + } + | { status: 'temporarily_unavailable' } + > { + 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 publicKeyPem: string; + let privateKeyPem: string; + let configuredPublicKey: Buffer; + try { + 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' }; + } + 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' }; + } + + return { + status: 'available', + keyId, + publicKeyPem, + privateKeyPem, + publicKeySha256: createHash('sha256').update(configuredPublicKey).digest('hex'), + }; + } +} diff --git a/services/git-token-service/src/gitlab-credential-service.test.ts b/services/git-token-service/src/gitlab-credential-service.test.ts new file mode 100644 index 0000000000..712e6ddd2f --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-service.test.ts @@ -0,0 +1,331 @@ +import { generateKeyPairSync } from 'node:crypto'; +import { encryptKeyedEnvelope } from '@kilocode/encryption'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, +} from '@kilocode/worker-utils/gitlab-credential'; +import { describe, expect, it } from 'vitest'; +import { GitLabCredentialCrypto } from './gitlab-credential-crypto.js'; +import { + GitLabCredentialService, + type GitLabCredentialStore, +} from './gitlab-credential-service.js'; + +const now = '2026-07-13T12:00:00.000Z'; + +function encryptedPatFixture() { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + const aad = buildGitLabPersonalAccessTokenAad({ + credentialId: 'credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-1' }, + authorizedByUserId: 'user-1', + credentialVersion: 1, + }); + return { + crypto: new GitLabCredentialCrypto({ + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: 'active', + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: Buffer.from(publicKey).toString('base64'), + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: Buffer.from(privateKey).toString('base64'), + }), + tokenEncrypted: encryptKeyedEnvelope( + 'glpat-secret', + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + { keyId: 'active', publicKeyPem: publicKey }, + aad + ), + }; +} + +function encryptedOAuthFixture() { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + const aad = buildGitLabOAuthCredentialAad({ + credentialId: 'oauth-credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-1' }, + authorizedByUserId: 'user-1', + credentialVersion: 1, + kind: 'access', + }); + return { + crypto: new GitLabCredentialCrypto({ + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: 'active', + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: Buffer.from(publicKey).toString('base64'), + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: Buffer.from(privateKey).toString('base64'), + }), + tokenEncrypted: encryptKeyedEnvelope( + 'oauth-access-secret', + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + { keyId: 'active', publicKeyPem: publicKey }, + aad + ), + }; +} + +function encryptedProjectTokenFixture() { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + const aad = buildGitLabProjectAccessTokenAad({ + credentialId: 'project-credential-1', + integrationId: 'integration-1', + providerBaseUrl: 'https://gitlab.example.com', + owner: { type: 'user', id: 'user-1' }, + providerResourceId: '42', + credentialVersion: 3, + }); + return { + crypto: new GitLabCredentialCrypto({ + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: 'active', + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: Buffer.from(publicKey).toString('base64'), + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: Buffer.from(privateKey).toString('base64'), + }), + tokenEncrypted: encryptKeyedEnvelope( + 'glpat-project-secret', + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + { keyId: 'active', publicKeyPem: publicKey }, + aad + ), + }; +} + +describe('GitLabCredentialService', () => { + it('resolves a matching encrypted integration PAT', async () => { + const fixture = encryptedPatFixture(); + const store: GitLabCredentialStore = { + findCredential: async () => ({ + parent: { + integrationId: 'integration-1', + platform: 'gitlab', + integrationType: 'pat', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + providerBaseUrl: 'https://gitlab.example.com', + }, + credential: { + id: 'credential-1', + platform_integration_id: 'integration-1', + token_encrypted: fixture.tokenEncrypted, + expires_at: null, + provider_credential_type: 'personal_access_token', + provider_resource_id: null, + provider_base_url: 'https://gitlab.example.com', + authorized_by_user_id: 'user-1', + provider_metadata: {}, + provider_scopes: null, + provider_verified_at: null, + credential_version: 1, + last_validated_at: null, + last_used_at: null, + created_at: now, + updated_at: now, + }, + }), + hasProjectCredentialCandidates: async () => false, + markUsed: async () => true, + }; + + await expect( + new GitLabCredentialService(store, fixture.crypto).getCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: 'integration-1' } + ) + ).resolves.toEqual({ + status: 'available', + token: 'glpat-secret', + instanceUrl: 'https://gitlab.example.com', + integrationId: 'integration-1', + glabIsOAuth2: false, + credentialId: 'credential-1', + credentialVersion: 1, + source: { type: 'integration' }, + }); + }); + + it('resolves a matching encrypted OAuth access token', async () => { + const fixture = encryptedOAuthFixture(); + const store: GitLabCredentialStore = { + findCredential: async () => ({ + parent: { + integrationId: 'integration-1', + platform: 'gitlab', + integrationType: 'oauth', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + providerBaseUrl: 'https://gitlab.example.com', + }, + credential: { + id: 'oauth-credential-1', + platform_integration_id: 'integration-1', + authorized_by_user_id: 'user-1', + provider_subject_id: '123', + provider_subject_login: 'octocat', + provider_base_url: 'https://gitlab.example.com', + access_token_encrypted: fixture.tokenEncrypted, + access_token_expires_at: '2027-07-13T14:00:00.000Z', + refresh_token_encrypted: 'encrypted-refresh-token', + refresh_token_expires_at: null, + oauth_client_secret_encrypted: null, + credential_version: 1, + revoked_at: null, + revocation_reason: null, + last_used_at: null, + created_at: now, + updated_at: now, + }, + }), + hasProjectCredentialCandidates: async () => false, + markUsed: async () => true, + }; + + await expect( + new GitLabCredentialService(store, fixture.crypto).getCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: 'integration-1' } + ) + ).resolves.toEqual({ + status: 'available', + token: 'oauth-access-secret', + instanceUrl: 'https://gitlab.example.com', + integrationId: 'integration-1', + glabIsOAuth2: true, + credentialId: 'oauth-credential-1', + credentialVersion: 1, + source: { type: 'integration' }, + }); + }); + + it('refreshes an expired OAuth credential through the locked refresher', async () => { + const fixture = encryptedOAuthFixture(); + const store: GitLabCredentialStore = { + findCredential: async () => ({ + parent: { + integrationId: 'integration-1', + platform: 'gitlab', + integrationType: 'oauth', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + providerBaseUrl: 'https://gitlab.example.com', + }, + credential: { + id: 'oauth-credential-1', + platform_integration_id: 'integration-1', + authorized_by_user_id: 'user-1', + provider_subject_id: '123', + provider_subject_login: 'octocat', + provider_base_url: 'https://gitlab.example.com', + access_token_encrypted: fixture.tokenEncrypted, + access_token_expires_at: '2020-01-01T00:00:00.000Z', + refresh_token_encrypted: 'encrypted-refresh-token', + refresh_token_expires_at: null, + oauth_client_secret_encrypted: null, + credential_version: 1, + revoked_at: null, + revocation_reason: null, + last_used_at: null, + created_at: now, + updated_at: now, + }, + }), + hasProjectCredentialCandidates: async () => false, + markUsed: async () => true, + }; + + await expect( + new GitLabCredentialService(store, fixture.crypto, { + refresh: async () => ({ + status: 'available', + token: 'refreshed-oauth-secret', + credentialVersion: 2, + }), + }).getCredential( + { userId: 'user-1' }, + { credential: 'integration', integrationId: 'integration-1' } + ) + ).resolves.toEqual({ + status: 'available', + token: 'refreshed-oauth-secret', + instanceUrl: 'https://gitlab.example.com', + integrationId: 'integration-1', + glabIsOAuth2: true, + credentialId: 'oauth-credential-1', + credentialVersion: 2, + source: { type: 'integration' }, + }); + }); + + it('resolves only the exact encrypted project credential', async () => { + const fixture = encryptedProjectTokenFixture(); + const store: GitLabCredentialStore = { + findCredential: async () => ({ + parent: { + integrationId: 'integration-1', + platform: 'gitlab', + integrationType: 'oauth', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + providerBaseUrl: 'https://gitlab.example.com', + }, + credential: { + id: 'project-credential-1', + platform_integration_id: 'integration-1', + token_encrypted: fixture.tokenEncrypted, + expires_at: null, + provider_credential_type: 'project_access_token', + provider_resource_id: '42', + provider_base_url: 'https://gitlab.example.com', + authorized_by_user_id: null, + provider_metadata: { + providerCredentialId: '314', + expiresOn: '2027-07-13', + }, + provider_scopes: null, + provider_verified_at: null, + credential_version: 3, + last_validated_at: null, + last_used_at: null, + created_at: now, + updated_at: now, + }, + }), + hasProjectCredentialCandidates: async () => false, + markUsed: async () => true, + }; + + await expect( + new GitLabCredentialService(store, fixture.crypto).getCredential( + { userId: 'user-1' }, + { credential: 'project-exact', integrationId: 'integration-1', projectId: '42' } + ) + ).resolves.toEqual({ + status: 'available', + token: 'glpat-project-secret', + instanceUrl: 'https://gitlab.example.com', + integrationId: 'integration-1', + glabIsOAuth2: false, + credentialId: 'project-credential-1', + credentialVersion: 3, + source: { type: 'project', projectId: '42' }, + }); + }); +}); diff --git a/services/git-token-service/src/gitlab-credential-service.ts b/services/git-token-service/src/gitlab-credential-service.ts new file mode 100644 index 0000000000..4e4f66c01f --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-service.ts @@ -0,0 +1,338 @@ +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + GitLabOAuthCredentialRowSchema, + GitLabPersonalAccessTokenCredentialRowSchema, + GitLabProjectAccessTokenCredentialRowSchema, + buildGitLabOAuthCredentialAad, + buildGitLabPersonalAccessTokenAad, + buildGitLabProjectAccessTokenAad, + type GitLabCredentialOwner, + type GitLabOAuthCredentialRow, +} from '@kilocode/worker-utils/gitlab-credential'; +import { normalizeGitLabInstanceUrl } from './gitlab-url.js'; +import type { GitLabCredentialCrypto } from './gitlab-credential-crypto.js'; + +export type GitLabCredentialActor = { userId: string; orgId?: string }; + +export type GitLabCredentialSelector = + | { credential: 'integration'; integrationId: string } + | { credential: 'project-exact'; integrationId: string; projectId: string }; + +export type GitLabCredentialParent = { + integrationId: string; + platform: string; + integrationType: string; + integrationStatus: string | null; + ownedByUserId: string | null; + ownedByOrganizationId: string | null; + providerBaseUrl: string | null; + providerSubjectId?: string | null; + providerSubjectLogin?: string | null; +}; + +export type GitLabCredentialFence = { + credentialTable: 'oauth' | 'access-token'; + integrationId: string; + credentialId: string; + credentialVersion: number; +}; + +export type GitLabCredentialStore = { + findCredential(input: { + actor: GitLabCredentialActor; + selector: GitLabCredentialSelector; + }): Promise<{ parent: GitLabCredentialParent; credential: unknown } | null>; + hasProjectCredentialCandidates(input: { + actor: GitLabCredentialActor; + integrationId: string; + }): Promise; + markUsed(fence: GitLabCredentialFence, at: string): Promise; +}; + +export type GitLabCredentialResult = + | { + status: 'available'; + token: string; + instanceUrl: string; + integrationId: string; + glabIsOAuth2: boolean; + credentialId: string; + credentialVersion: number; + source: { type: 'integration' } | { type: 'project'; projectId: string }; + } + | { status: 'not_connected' } + | { status: 'credential_absent' } + | { status: 'reconnect_required' } + | { status: 'temporarily_unavailable' }; + +export type GitLabOAuthCredentialRefreshResult = + | { status: 'available'; token: string; credentialVersion: number } + | { status: 'reconnect_required' } + | { status: 'temporarily_unavailable' }; + +export type GitLabOAuthCredentialRefresher = { + refresh(input: { + actor: GitLabCredentialActor; + parent: GitLabCredentialParent; + owner: GitLabCredentialOwner; + credential: GitLabOAuthCredentialRow; + }): Promise; +}; + +const OAUTH_REFRESH_BUFFER_MS = 5 * 60 * 1000; + +function needsOAuthRefresh(expiresAt: string | null): boolean { + return !expiresAt || new Date(expiresAt).getTime() - Date.now() <= OAUTH_REFRESH_BUFFER_MS; +} + +function parentOwner( + parent: GitLabCredentialParent, + actor: GitLabCredentialActor +): GitLabCredentialOwner | null { + if (actor.orgId) { + if (parent.ownedByOrganizationId !== actor.orgId || parent.ownedByUserId !== null) { + return null; + } + return { type: 'org', id: actor.orgId }; + } + if (parent.ownedByUserId !== actor.userId || parent.ownedByOrganizationId !== null) { + return null; + } + return { type: 'user', id: actor.userId }; +} + +function canonicalProviderBaseUrl(value: string | null): string | null { + if (!value) return null; + const normalized = normalizeGitLabInstanceUrl(value); + return normalized === value ? normalized : null; +} + +export class GitLabCredentialService { + constructor( + private store: GitLabCredentialStore, + private crypto: Pick, + private oauthRefresher?: GitLabOAuthCredentialRefresher + ) {} + + hasProjectCredentialCandidates( + actor: GitLabCredentialActor, + integrationId: string + ): Promise { + return this.store.hasProjectCredentialCandidates({ actor, integrationId }); + } + + async getCredential( + actor: GitLabCredentialActor, + selector: GitLabCredentialSelector + ): Promise { + let loaded: Awaited>; + try { + loaded = await this.store.findCredential({ actor, selector }); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (!loaded) return { status: 'not_connected' }; + + const { parent } = loaded; + const owner = parentOwner(parent, actor); + const instanceUrl = canonicalProviderBaseUrl(parent.providerBaseUrl); + if ( + !owner || + !instanceUrl || + parent.integrationId !== selector.integrationId || + parent.platform !== 'gitlab' || + parent.integrationStatus !== 'active' + ) { + return { status: 'reconnect_required' }; + } + if (loaded.credential === null || loaded.credential === undefined) { + return { status: 'credential_absent' }; + } + + if (selector.credential === 'project-exact') { + const parsed = GitLabProjectAccessTokenCredentialRowSchema.safeParse(loaded.credential); + if (!parsed.success) return { status: 'reconnect_required' }; + const credential = parsed.data; + if ( + credential.platform_integration_id !== parent.integrationId || + credential.provider_resource_id !== selector.projectId || + credential.provider_base_url !== instanceUrl + ) { + return { status: 'reconnect_required' }; + } + + const decrypted = await this.crypto.decrypt({ + ciphertext: credential.token_encrypted, + scheme: GITLAB_PROJECT_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: buildGitLabProjectAccessTokenAad({ + credentialId: credential.id, + integrationId: credential.platform_integration_id, + providerBaseUrl: credential.provider_base_url, + owner, + providerResourceId: credential.provider_resource_id, + credentialVersion: credential.credential_version, + }), + }); + if (decrypted.status === 'temporarily_unavailable') return decrypted; + if (decrypted.status === 'unreadable') return { status: 'reconnect_required' }; + + const fence = { + credentialTable: 'access-token' as const, + integrationId: parent.integrationId, + credentialId: credential.id, + credentialVersion: credential.credential_version, + }; + try { + if (!(await this.store.markUsed(fence, new Date().toISOString()))) { + return { status: 'reconnect_required' }; + } + } catch { + return { status: 'temporarily_unavailable' }; + } + return { + status: 'available', + token: decrypted.token, + instanceUrl, + integrationId: parent.integrationId, + glabIsOAuth2: false, + credentialId: credential.id, + credentialVersion: credential.credential_version, + source: { type: 'project', projectId: credential.provider_resource_id }, + }; + } + + if (parent.integrationType === 'oauth') { + const parsed = GitLabOAuthCredentialRowSchema.safeParse(loaded.credential); + if (!parsed.success) return { status: 'reconnect_required' }; + const credential = parsed.data; + if ( + credential.platform_integration_id !== parent.integrationId || + credential.provider_base_url !== instanceUrl || + credential.revoked_at !== null || + (parent.providerSubjectId !== undefined && + credential.provider_subject_id !== parent.providerSubjectId) || + (parent.providerSubjectLogin !== undefined && + credential.provider_subject_login !== parent.providerSubjectLogin) || + (owner.type === 'user' && credential.authorized_by_user_id !== owner.id) + ) { + return { status: 'reconnect_required' }; + } + + if (needsOAuthRefresh(credential.access_token_expires_at)) { + if (!this.oauthRefresher) return { status: 'temporarily_unavailable' }; + let refreshed: GitLabOAuthCredentialRefreshResult; + try { + refreshed = await this.oauthRefresher.refresh({ actor, parent, owner, credential }); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (refreshed.status !== 'available') return refreshed; + return { + status: 'available', + token: refreshed.token, + instanceUrl, + integrationId: parent.integrationId, + glabIsOAuth2: true, + credentialId: credential.id, + credentialVersion: refreshed.credentialVersion, + source: { type: 'integration' }, + }; + } + + const decrypted = await this.crypto.decrypt({ + ciphertext: credential.access_token_encrypted, + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + aad: buildGitLabOAuthCredentialAad({ + credentialId: credential.id, + integrationId: credential.platform_integration_id, + providerBaseUrl: credential.provider_base_url, + owner, + authorizedByUserId: credential.authorized_by_user_id, + credentialVersion: credential.credential_version, + kind: 'access', + }), + }); + if (decrypted.status === 'temporarily_unavailable') return decrypted; + if (decrypted.status === 'unreadable') return { status: 'reconnect_required' }; + + const fence = { + credentialTable: 'oauth' as const, + integrationId: parent.integrationId, + credentialId: credential.id, + credentialVersion: credential.credential_version, + }; + try { + if (!(await this.store.markUsed(fence, new Date().toISOString()))) { + return { status: 'reconnect_required' }; + } + } catch { + return { status: 'temporarily_unavailable' }; + } + return { + status: 'available', + token: decrypted.token, + instanceUrl, + integrationId: parent.integrationId, + glabIsOAuth2: true, + credentialId: credential.id, + credentialVersion: credential.credential_version, + source: { type: 'integration' }, + }; + } + + if (parent.integrationType !== 'pat') return { status: 'reconnect_required' }; + + const parsed = GitLabPersonalAccessTokenCredentialRowSchema.safeParse(loaded.credential); + if (!parsed.success) return { status: 'reconnect_required' }; + const credential = parsed.data; + if ( + credential.platform_integration_id !== parent.integrationId || + credential.provider_base_url !== instanceUrl || + (owner.type === 'user' && credential.authorized_by_user_id !== owner.id) + ) { + return { status: 'reconnect_required' }; + } + + const decrypted = await this.crypto.decrypt({ + ciphertext: credential.token_encrypted, + scheme: GITLAB_PERSONAL_ACCESS_TOKEN_ENVELOPE_SCHEME, + aad: buildGitLabPersonalAccessTokenAad({ + credentialId: credential.id, + integrationId: credential.platform_integration_id, + providerBaseUrl: credential.provider_base_url, + owner, + authorizedByUserId: credential.authorized_by_user_id, + credentialVersion: credential.credential_version, + }), + }); + if (decrypted.status === 'temporarily_unavailable') return decrypted; + if (decrypted.status === 'unreadable') return { status: 'reconnect_required' }; + + const fence = { + credentialTable: 'access-token' as const, + integrationId: parent.integrationId, + credentialId: credential.id, + credentialVersion: credential.credential_version, + }; + try { + if (!(await this.store.markUsed(fence, new Date().toISOString()))) { + return { status: 'reconnect_required' }; + } + } catch { + return { status: 'temporarily_unavailable' }; + } + + return { + status: 'available', + token: decrypted.token, + instanceUrl, + integrationId: parent.integrationId, + glabIsOAuth2: false, + credentialId: credential.id, + credentialVersion: credential.credential_version, + source: { type: 'integration' }, + }; + } +} diff --git a/services/git-token-service/src/gitlab-credential-store.ts b/services/git-token-service/src/gitlab-credential-store.ts new file mode 100644 index 0000000000..3d5e23e881 --- /dev/null +++ b/services/git-token-service/src/gitlab-credential-store.ts @@ -0,0 +1,152 @@ +import { getWorkerDb, type WorkerDb } from '@kilocode/db/client'; +import { platform_access_token_credentials, platform_oauth_credentials } from '@kilocode/db/schema'; +import { and, eq, isNull } from 'drizzle-orm'; +import { DEFAULT_GITLAB_INSTANCE_URL } from './gitlab-constants.js'; +import { GitLabLookupService, type GitLabLookupParams } from './gitlab-lookup-service.js'; +import type { + GitLabCredentialFence, + GitLabCredentialSelector, + GitLabCredentialStore, +} from './gitlab-credential-service.js'; +import { normalizeGitLabInstanceUrl } from './gitlab-url.js'; + +export class DrizzleGitLabCredentialStore implements GitLabCredentialStore { + private lookupService: GitLabLookupService; + + constructor(private env: CloudflareEnv) { + this.lookupService = new GitLabLookupService(env); + } + + async findCredential(input: { actor: GitLabLookupParams; selector: GitLabCredentialSelector }) { + const integration = await this.lookupService.findGitLabIntegration( + input.actor, + input.selector.integrationId + ); + if (!integration.success) { + if (integration.reason === 'database_not_configured') { + throw new Error('GitLab credential database is unavailable'); + } + return null; + } + + const providerBaseUrl = normalizeGitLabInstanceUrl( + integration.metadata.gitlab_instance_url ?? DEFAULT_GITLAB_INSTANCE_URL + ); + const db = this.getDb(); + let credential: unknown = null; + if (input.selector.credential === 'project-exact') { + [credential] = await db + .select() + .from(platform_access_token_credentials) + .where( + and( + eq( + platform_access_token_credentials.platform_integration_id, + integration.integrationId + ), + eq(platform_access_token_credentials.provider_credential_type, 'project_access_token'), + eq(platform_access_token_credentials.provider_resource_id, input.selector.projectId) + ) + ) + .limit(1); + } else if (integration.integrationType === 'oauth') { + [credential] = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, integration.integrationId)) + .limit(1); + } else { + [credential] = await db + .select() + .from(platform_access_token_credentials) + .where( + and( + eq( + platform_access_token_credentials.platform_integration_id, + integration.integrationId + ), + isNull(platform_access_token_credentials.provider_resource_id) + ) + ) + .limit(1); + } + + return { + parent: { + integrationId: integration.integrationId, + platform: 'gitlab', + integrationType: integration.integrationType, + integrationStatus: 'active', + ownedByUserId: input.actor.orgId ? null : input.actor.userId, + ownedByOrganizationId: input.actor.orgId ?? null, + providerBaseUrl, + providerSubjectId: integration.accountId, + providerSubjectLogin: integration.accountLogin, + }, + credential, + }; + } + + async markUsed(fence: GitLabCredentialFence, at: string): Promise { + const db = this.getDb(); + if (fence.credentialTable === 'oauth') { + const updated = await db + .update(platform_oauth_credentials) + .set({ last_used_at: at }) + .where( + and( + eq(platform_oauth_credentials.id, fence.credentialId), + eq(platform_oauth_credentials.platform_integration_id, fence.integrationId), + eq(platform_oauth_credentials.credential_version, fence.credentialVersion) + ) + ) + .returning({ id: platform_oauth_credentials.id }); + return updated.length === 1; + } + + const updated = await 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.platform_integration_id, fence.integrationId), + eq(platform_access_token_credentials.credential_version, fence.credentialVersion) + ) + ) + .returning({ id: platform_access_token_credentials.id }); + return updated.length === 1; + } + + async hasProjectCredentialCandidates(input: { + actor: GitLabLookupParams; + integrationId: string; + }): Promise { + const integration = await this.lookupService.findGitLabIntegration( + input.actor, + input.integrationId + ); + if (!integration.success) { + if (integration.reason === 'database_not_configured') { + throw new Error('GitLab credential database is unavailable'); + } + return false; + } + const rows = await this.getDb() + .select({ id: platform_access_token_credentials.id }) + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.platform_integration_id, integration.integrationId), + eq(platform_access_token_credentials.provider_credential_type, 'project_access_token') + ) + ) + .limit(1); + return rows.length > 0; + } + + private getDb(): WorkerDb { + if (!this.env.HYPERDRIVE) throw new Error('Hyperdrive not configured'); + return getWorkerDb(this.env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); + } +} diff --git a/services/git-token-service/src/gitlab-oauth-credential-refresher.test.ts b/services/git-token-service/src/gitlab-oauth-credential-refresher.test.ts new file mode 100644 index 0000000000..933e0e4d56 --- /dev/null +++ b/services/git-token-service/src/gitlab-oauth-credential-refresher.test.ts @@ -0,0 +1,275 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { platform_integrations, platform_oauth_credentials } from '@kilocode/db/schema'; +import { GitLabCredentialCrypto } from './gitlab-credential-crypto.js'; +import { GitLabOAuthCredentialRefresher } from './gitlab-oauth-credential-refresher.js'; + +const { getWorkerDbMock } = vi.hoisted(() => ({ getWorkerDbMock: vi.fn() })); + +vi.mock('@kilocode/db/client', () => ({ getWorkerDb: getWorkerDbMock })); + +function refreshInput() { + return { + actor: { userId: 'user-1' }, + owner: { type: 'user' as const, id: 'user-1' }, + parent: { + integrationId: 'integration-1', + platform: 'gitlab', + integrationType: 'oauth', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + providerBaseUrl: 'https://gitlab.example.com', + }, + credential: { + id: 'credential-1', + platform_integration_id: 'integration-1', + authorized_by_user_id: 'user-1', + provider_subject_id: '123', + provider_subject_login: 'octocat', + provider_base_url: 'https://gitlab.example.com', + access_token_encrypted: 'encrypted-access', + access_token_expires_at: '2020-01-01T00:00:00.000Z', + refresh_token_encrypted: 'encrypted-refresh', + refresh_token_expires_at: null, + oauth_client_secret_encrypted: null, + credential_version: 1, + revoked_at: null, + revocation_reason: null, + last_used_at: null, + created_at: '2026-07-13T12:00:00.000Z', + updated_at: '2026-07-13T12:00:00.000Z', + }, + }; +} + +describe('GitLabOAuthCredentialRefresher', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('classifies missing database configuration as temporary unavailability', async () => { + await expect(new GitLabOAuthCredentialRefresher({}).refresh(refreshInput())).resolves.toEqual({ + status: 'temporarily_unavailable', + }); + }); + + it('rejects a replacement credential row even when its version matches the candidate', async () => { + const input = refreshInput(); + const loaded = { + credential: { + ...input.credential, + id: 'replacement-credential', + access_token_expires_at: '2099-01-01T00:00:00.000Z', + }, + integrationId: input.parent.integrationId, + platform: 'gitlab', + integrationType: 'oauth', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + accountId: '123', + accountLogin: 'octocat', + metadata: { gitlab_instance_url: 'https://gitlab.example.com' }, + }; + const tx = { + execute: vi.fn(), + select: vi.fn(() => ({ + from: vi.fn(() => ({ + innerJoin: vi.fn(() => ({ + where: vi.fn(() => ({ limit: vi.fn().mockResolvedValue([loaded]) })), + })), + })), + })), + }; + getWorkerDbMock.mockReturnValue({ + transaction: (callback: (transaction: typeof tx) => unknown) => callback(tx), + }); + + await expect( + new GitLabOAuthCredentialRefresher({ + HYPERDRIVE: { connectionString: 'postgres://test' } as Hyperdrive, + }).refresh(input) + ).resolves.toEqual({ status: 'reconnect_required' }); + }); + + it('refreshes encrypted credentials without rewriting plaintext', async () => { + const input = refreshInput(); + const loaded = { + credential: input.credential, + integrationId: input.parent.integrationId, + platform: 'gitlab', + integrationType: 'oauth', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + accountId: '123', + accountLogin: 'octocat', + metadata: { + access_token: 'old-plaintext-access', + refresh_token: 'old-plaintext-refresh', + gitlab_instance_url: 'https://gitlab.example.com', + }, + }; + const integrationUpdates: Record[] = []; + const tx = { + execute: vi.fn(), + select: vi.fn(() => ({ + from: vi.fn(() => ({ + innerJoin: vi.fn(() => ({ + where: vi.fn(() => ({ limit: vi.fn().mockResolvedValue([loaded]) })), + })), + })), + })), + update: vi.fn((table: unknown) => ({ + set: vi.fn((values: Record) => ({ + where: vi.fn(() => { + if (table === platform_integrations) { + integrationUpdates.push(values); + return Promise.resolve(); + } + if (table === platform_oauth_credentials) { + return { returning: vi.fn().mockResolvedValue([{ id: input.credential.id }]) }; + } + throw new Error('Unexpected table'); + }), + })), + })), + }; + getWorkerDbMock.mockReturnValue({ + transaction: (callback: (transaction: typeof tx) => unknown) => callback(tx), + }); + const crypto = new GitLabCredentialCrypto({}); + vi.spyOn(crypto, 'decrypt').mockResolvedValue({ status: 'available', token: 'refresh-token' }); + vi.spyOn(crypto, 'encrypt') + .mockResolvedValueOnce({ status: 'available', ciphertext: 'new-access-envelope' }) + .mockResolvedValueOnce({ status: 'available', ciphertext: 'new-refresh-envelope' }); + + await expect( + new GitLabOAuthCredentialRefresher( + { + HYPERDRIVE: { connectionString: 'postgres://test' } as Hyperdrive, + GITLAB_CLIENT_ID: 'client-id', + GITLAB_CLIENT_SECRET: 'client-secret', + }, + { + crypto, + now: () => new Date('2026-07-13T12:00:00.000Z'), + fetch: vi.fn().mockResolvedValue( + Response.json({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + token_type: 'Bearer', + expires_in: 3600, + created_at: 1_784_118_000, + scope: 'api read_user', + }) + ), + } + ).refresh(input) + ).resolves.toEqual({ + status: 'available', + token: 'new-access-token', + credentialVersion: 2, + }); + expect(integrationUpdates).toEqual([ + expect.objectContaining({ integration_type: 'oauth', scopes: ['api', 'read_user'] }), + ]); + expect(integrationUpdates[0]).not.toHaveProperty('metadata'); + }); + + it('promotes an expired legacy OAuth credential into one encrypted row without rewriting plaintext', async () => { + const insertedRows: Record[] = []; + const integrationUpdates: Record[] = []; + const loaded = { + credential: null, + integrationId: 'integration-1', + platform: 'gitlab', + integrationType: 'oauth', + integrationStatus: 'active', + ownedByUserId: 'user-1', + ownedByOrganizationId: null, + accountId: '123', + accountLogin: 'octocat', + metadata: { + access_token: 'legacy-access-token', + refresh_token: 'legacy-refresh-token', + token_expires_at: '2020-01-01T00:00:00.000Z', + gitlab_instance_url: 'https://gitlab.example.com', + auth_type: 'oauth', + }, + }; + const tx = { + execute: vi.fn(), + select: vi.fn(() => ({ + from: vi.fn(() => ({ + leftJoin: vi.fn(() => ({ + where: vi.fn(() => ({ limit: vi.fn().mockResolvedValue([loaded]) })), + })), + })), + })), + insert: vi.fn(() => ({ + values: vi.fn((values: Record) => { + insertedRows.push(values); + return { + onConflictDoNothing: vi.fn(() => ({ + returning: vi.fn().mockResolvedValue([{ id: values.id }]), + })), + }; + }), + })), + update: vi.fn((table: unknown) => ({ + set: vi.fn((values: Record) => ({ + where: vi.fn(() => { + if (table === platform_integrations) integrationUpdates.push(values); + return Promise.resolve(); + }), + })), + })), + }; + getWorkerDbMock.mockReturnValue({ + transaction: (callback: (transaction: typeof tx) => unknown) => callback(tx), + }); + const crypto = new GitLabCredentialCrypto({}); + vi.spyOn(crypto, 'encrypt') + .mockResolvedValueOnce({ status: 'available', ciphertext: 'access-envelope' }) + .mockResolvedValueOnce({ status: 'available', ciphertext: 'refresh-envelope' }); + + await expect( + new GitLabOAuthCredentialRefresher( + { + HYPERDRIVE: { connectionString: 'postgres://test' } as Hyperdrive, + GITLAB_CLIENT_ID: 'client-id', + GITLAB_CLIENT_SECRET: 'client-secret', + }, + { + crypto, + now: () => new Date('2026-07-13T12:00:00.000Z'), + fetch: vi.fn().mockResolvedValue( + Response.json({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + token_type: 'Bearer', + expires_in: 3600, + created_at: 1_784_118_000, + scope: 'api read_user', + }) + ), + } + ).promoteLegacy({ actor: { userId: 'user-1' }, integrationId: 'integration-1' }) + ).resolves.toEqual({ + status: 'available', + token: 'new-access-token', + instanceUrl: 'https://gitlab.example.com', + }); + expect(insertedRows).toEqual([ + expect.objectContaining({ + platform_integration_id: 'integration-1', + access_token_encrypted: 'access-envelope', + refresh_token_encrypted: 'refresh-envelope', + credential_version: 1, + }), + ]); + expect(JSON.stringify(insertedRows)).not.toContain('new-access-token'); + expect(integrationUpdates[0]).not.toHaveProperty('metadata'); + }); +}); diff --git a/services/git-token-service/src/gitlab-oauth-credential-refresher.ts b/services/git-token-service/src/gitlab-oauth-credential-refresher.ts new file mode 100644 index 0000000000..62e44e983c --- /dev/null +++ b/services/git-token-service/src/gitlab-oauth-credential-refresher.ts @@ -0,0 +1,586 @@ +import { getWorkerDb } from '@kilocode/db/client'; +import { platform_integrations, platform_oauth_credentials } from '@kilocode/db/schema'; +import { + GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + GitLabOAuthCredentialRowSchema, + buildGitLabOAuthCredentialAad, + type GitLabCredentialOwner, +} from '@kilocode/worker-utils/gitlab-credential'; +import { and, eq, isNull, sql } from 'drizzle-orm'; +import { z } from 'zod'; +import { + GitLabCredentialCrypto, + type GitLabCredentialCryptoEnv, +} from './gitlab-credential-crypto.js'; +import type { GitLabOAuthCredentialRefresher as GitLabOAuthCredentialRefresherContract } from './gitlab-credential-service.js'; +import { normalizeGitLabInstanceUrl } from './gitlab-url.js'; + +type Secret = SecretsStoreSecret | string | undefined; +type GitLabOAuthCredentialRefresherEnv = GitLabCredentialCryptoEnv & { + HYPERDRIVE?: Hyperdrive; + GITLAB_CLIENT_ID?: Secret; + GITLAB_CLIENT_SECRET?: Secret; +}; + +type RefreshInput = Parameters[0]; +type RefreshResult = Awaited>; + +export type GitLabLegacyOAuthPromotionResult = + | { status: 'available'; token: string; instanceUrl: string } + | { status: 'encrypted_credential_available' } + | { status: 'reconnect_required' } + | { status: 'temporarily_unavailable' }; + +const OAuthRefreshResponseSchema = 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), + created_at: z.number().int().positive(), + scope: z.string(), + }) + .strict(); + +const OAuthRefreshErrorSchema = z.object({ error: z.string() }); +const GitLabRefreshMetadataSchema = z + .object({ + access_token: z.string().optional(), + refresh_token: z.string().optional(), + token_expires_at: z.string().optional(), + gitlab_instance_url: z.string().optional(), + client_id: z.string().min(1).optional(), + client_secret: z.string().optional(), + auth_type: z.enum(['oauth', 'pat']).optional(), + }) + .passthrough(); + +const REFRESH_BUFFER_MS = 5 * 60 * 1000; +const MAX_REFRESH_RESPONSE_BYTES = 64_000; +type OAuthProviderRefreshResult = + | { status: 'available'; data: z.infer } + | { status: 'invalid_grant' } + | { status: 'temporarily_unavailable' }; + +async function resolveSecret(secret: Secret): Promise { + if (!secret) return null; + const value = typeof secret === 'string' ? secret : await secret.get(); + return value || null; +} + +async function readBoundedJson(response: Response): Promise { + if (!response.body) throw new Error('invalid_response'); + const contentLength = response.headers.get('Content-Length'); + if ( + contentLength && + (!/^[0-9]+$/.test(contentLength) || Number(contentLength) > MAX_REFRESH_RESPONSE_BYTES) + ) { + throw new Error('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; + if (!(chunk.value instanceof Uint8Array)) throw new Error('invalid_response'); + totalBytes += chunk.value.byteLength; + if (totalBytes > MAX_REFRESH_RESPONSE_BYTES) { + await reader.cancel(); + throw new Error('invalid_response'); + } + chunks.push(chunk.value); + } + } finally { + reader.releaseLock(); + } + const body = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + return JSON.parse(new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(body)); +} + +function ownsParent( + row: { + ownedByUserId: string | null; + ownedByOrganizationId: string | null; + }, + input: RefreshInput +): boolean { + return input.owner.type === 'org' + ? row.ownedByOrganizationId === input.owner.id && row.ownedByUserId === null + : row.ownedByUserId === input.owner.id && row.ownedByOrganizationId === null; +} + +export class GitLabOAuthCredentialRefresher implements GitLabOAuthCredentialRefresherContract { + private crypto: GitLabCredentialCrypto; + + constructor( + private env: GitLabOAuthCredentialRefresherEnv, + private dependencies: { + crypto?: GitLabCredentialCrypto; + fetch?: typeof fetch; + now?: () => Date; + } = {} + ) { + this.crypto = dependencies.crypto ?? new GitLabCredentialCrypto(env); + } + + async promoteLegacy(input: { + actor: { userId: string; orgId?: string }; + integrationId: string; + }): Promise { + if (!this.env.HYPERDRIVE) return { status: 'temporarily_unavailable' }; + const db = getWorkerDb(this.env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); + try { + return await db.transaction(async tx => { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`gitlab-integration:${input.integrationId}`}, 0))` + ); + const [loaded] = await tx + .select({ + credential: platform_oauth_credentials, + integrationId: platform_integrations.id, + platform: platform_integrations.platform, + integrationType: platform_integrations.integration_type, + integrationStatus: platform_integrations.integration_status, + ownedByUserId: platform_integrations.owned_by_user_id, + ownedByOrganizationId: platform_integrations.owned_by_organization_id, + accountId: platform_integrations.platform_account_id, + accountLogin: platform_integrations.platform_account_login, + metadata: platform_integrations.metadata, + }) + .from(platform_integrations) + .leftJoin( + platform_oauth_credentials, + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id) + ) + .where(eq(platform_integrations.id, input.integrationId)) + .limit(1); + if (!loaded) return { status: 'reconnect_required' } as const; + if (loaded.credential !== null) { + return { status: 'encrypted_credential_available' } as const; + } + + const metadata = GitLabRefreshMetadataSchema.safeParse(loaded.metadata ?? {}); + const owner: GitLabCredentialOwner | null = input.actor.orgId + ? loaded.ownedByOrganizationId === input.actor.orgId && loaded.ownedByUserId === null + ? { type: 'org', id: input.actor.orgId } + : null + : loaded.ownedByUserId === input.actor.userId && loaded.ownedByOrganizationId === null + ? { type: 'user', id: input.actor.userId } + : null; + const instanceUrl = metadata.success + ? normalizeGitLabInstanceUrl(metadata.data.gitlab_instance_url ?? 'https://gitlab.com') + : null; + if ( + !metadata.success || + !owner || + !instanceUrl || + loaded.platform !== 'gitlab' || + loaded.integrationType !== 'oauth' || + loaded.integrationStatus !== 'active' || + !loaded.accountId || + !loaded.accountLogin || + metadata.data.auth_type !== 'oauth' || + !metadata.data.access_token + ) { + return { status: 'reconnect_required' } as const; + } + + const now = (this.dependencies.now ?? (() => new Date()))(); + if ( + metadata.data.token_expires_at && + new Date(metadata.data.token_expires_at).getTime() - now.getTime() > REFRESH_BUFFER_MS + ) { + return { + status: 'available', + token: metadata.data.access_token, + instanceUrl, + } as const; + } + if (!metadata.data.refresh_token) return { status: 'reconnect_required' } as const; + + const clientId = + metadata.data.client_id ?? (await resolveSecret(this.env.GITLAB_CLIENT_ID)); + const clientSecret = metadata.data.client_id + ? (metadata.data.client_secret ?? null) + : await resolveSecret(this.env.GITLAB_CLIENT_SECRET); + if (!clientId || !clientSecret) return { status: 'temporarily_unavailable' } as const; + + const refreshed = await this.requestOAuthRefresh({ + instanceUrl, + clientId, + clientSecret, + refreshToken: metadata.data.refresh_token, + }); + if (refreshed.status === 'invalid_grant') { + return { status: 'reconnect_required' } as const; + } + if (refreshed.status !== 'available') { + return { status: 'temporarily_unavailable' } as const; + } + + const credentialId = crypto.randomUUID(); + const credentialVersion = 1; + const authorizedByUserId = owner.type === 'user' ? owner.id : null; + const encrypt = (plaintext: string, kind: 'access' | 'refresh' | 'oauth-client-secret') => + this.crypto.encrypt({ + plaintext, + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + aad: buildGitLabOAuthCredentialAad({ + credentialId, + integrationId: loaded.integrationId, + providerBaseUrl: instanceUrl, + owner, + authorizedByUserId, + credentialVersion, + kind, + }), + }); + const [accessTokenEncrypted, refreshTokenEncrypted, clientSecretEncrypted] = + await Promise.all([ + encrypt(refreshed.data.access_token, 'access'), + encrypt(refreshed.data.refresh_token, 'refresh'), + metadata.data.client_id + ? encrypt(clientSecret, 'oauth-client-secret') + : Promise.resolve(null), + ]); + if ( + accessTokenEncrypted.status !== 'available' || + refreshTokenEncrypted.status !== 'available' || + (clientSecretEncrypted && clientSecretEncrypted.status !== 'available') + ) { + return { status: 'temporarily_unavailable' } as const; + } + const expiresAt = new Date( + (refreshed.data.created_at + refreshed.data.expires_in) * 1000 + ).toISOString(); + const inserted = await tx + .insert(platform_oauth_credentials) + .values({ + id: credentialId, + platform_integration_id: loaded.integrationId, + authorized_by_user_id: authorizedByUserId, + provider_subject_id: loaded.accountId, + provider_subject_login: loaded.accountLogin, + provider_base_url: instanceUrl, + access_token_encrypted: accessTokenEncrypted.ciphertext, + access_token_expires_at: expiresAt, + refresh_token_encrypted: refreshTokenEncrypted.ciphertext, + refresh_token_expires_at: null, + oauth_client_secret_encrypted: clientSecretEncrypted?.ciphertext ?? null, + credential_version: credentialVersion, + last_used_at: now.toISOString(), + }) + .onConflictDoNothing() + .returning({ id: platform_oauth_credentials.id }); + if (inserted.length === 0) { + return { status: 'encrypted_credential_available' } as const; + } + + await tx + .update(platform_integrations) + .set({ + integration_type: 'oauth', + scopes: refreshed.data.scope.split(/\s+/).filter(Boolean), + updated_at: now.toISOString(), + }) + .where(eq(platform_integrations.id, loaded.integrationId)); + return { + status: 'available', + token: refreshed.data.access_token, + instanceUrl, + } as const; + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + } + + async refresh(input: RefreshInput): Promise { + if (!this.env.HYPERDRIVE) return { status: 'temporarily_unavailable' }; + const db = getWorkerDb(this.env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); + try { + return await db.transaction(async tx => { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`gitlab-integration:${input.parent.integrationId}`}, 0))` + ); + const [loaded] = await tx + .select({ + credential: platform_oauth_credentials, + integrationId: platform_integrations.id, + platform: platform_integrations.platform, + integrationType: platform_integrations.integration_type, + integrationStatus: platform_integrations.integration_status, + ownedByUserId: platform_integrations.owned_by_user_id, + ownedByOrganizationId: platform_integrations.owned_by_organization_id, + accountId: platform_integrations.platform_account_id, + accountLogin: platform_integrations.platform_account_login, + metadata: platform_integrations.metadata, + }) + .from(platform_integrations) + .innerJoin( + platform_oauth_credentials, + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id) + ) + .where(eq(platform_integrations.id, input.parent.integrationId)) + .limit(1); + if (!loaded) return { status: 'reconnect_required' }; + + const parsedCredential = GitLabOAuthCredentialRowSchema.safeParse(loaded.credential); + const parsedMetadata = GitLabRefreshMetadataSchema.safeParse(loaded.metadata ?? {}); + if (!parsedCredential.success || !parsedMetadata.success) { + return { status: 'reconnect_required' }; + } + const credential = parsedCredential.data; + const metadata = parsedMetadata.data; + const instanceUrl = normalizeGitLabInstanceUrl( + metadata.gitlab_instance_url ?? 'https://gitlab.com' + ); + if ( + !instanceUrl || + instanceUrl !== credential.provider_base_url || + loaded.platform !== 'gitlab' || + loaded.integrationType !== 'oauth' || + loaded.integrationStatus !== 'active' || + credential.id !== input.credential.id || + loaded.integrationId !== credential.platform_integration_id || + loaded.accountId !== credential.provider_subject_id || + loaded.accountLogin !== credential.provider_subject_login || + credential.revoked_at !== null || + !ownsParent(loaded, input) + ) { + return { status: 'reconnect_required' }; + } + + const now = (this.dependencies.now ?? (() => new Date()))(); + if ( + credential.credential_version !== input.credential.credential_version || + (credential.access_token_expires_at && + new Date(credential.access_token_expires_at).getTime() - now.getTime() > + REFRESH_BUFFER_MS) + ) { + const current = await this.decryptOAuthSecret(credential, input, 'access'); + return current.status === 'available' + ? { + status: 'available', + token: current.token, + credentialVersion: credential.credential_version, + } + : current.status === 'temporarily_unavailable' + ? current + : { status: 'reconnect_required' }; + } + + const refreshToken = credential.refresh_token_encrypted + ? await this.decryptOAuthSecret(credential, input, 'refresh') + : { status: 'unreadable' as const }; + if (refreshToken.status === 'temporarily_unavailable') return refreshToken; + if (refreshToken.status !== 'available') return { status: 'reconnect_required' }; + + const clientId = metadata.client_id ?? (await resolveSecret(this.env.GITLAB_CLIENT_ID)); + let clientSecret: string | null; + if (metadata.client_id) { + if (!credential.oauth_client_secret_encrypted) return { status: 'reconnect_required' }; + const customSecret = await this.decryptOAuthSecret( + credential, + input, + 'oauth-client-secret' + ); + if (customSecret.status === 'temporarily_unavailable') return customSecret; + if (customSecret.status !== 'available') return { status: 'reconnect_required' }; + clientSecret = customSecret.token; + } else { + clientSecret = await resolveSecret(this.env.GITLAB_CLIENT_SECRET); + } + if (!clientId || !clientSecret) return { status: 'temporarily_unavailable' }; + + const refreshed = await this.requestOAuthRefresh({ + instanceUrl, + clientId, + clientSecret, + refreshToken: refreshToken.token, + }); + if (refreshed.status === 'invalid_grant') { + await tx + .update(platform_oauth_credentials) + .set({ + revoked_at: now.toISOString(), + revocation_reason: 'refresh_token_rejected', + }) + .where( + and( + eq(platform_oauth_credentials.id, credential.id), + eq(platform_oauth_credentials.credential_version, credential.credential_version), + isNull(platform_oauth_credentials.revoked_at) + ) + ); + return { status: 'reconnect_required' }; + } + if (refreshed.status !== 'available') return { status: 'temporarily_unavailable' }; + const nextVersion = credential.credential_version + 1; + const [accessTokenEncrypted, refreshTokenEncrypted] = await Promise.all([ + this.encryptOAuthSecret( + refreshed.data.access_token, + credential, + input, + 'access', + nextVersion + ), + this.encryptOAuthSecret( + refreshed.data.refresh_token, + credential, + input, + 'refresh', + nextVersion + ), + ]); + if ( + accessTokenEncrypted.status !== 'available' || + refreshTokenEncrypted.status !== 'available' + ) { + return { status: 'temporarily_unavailable' }; + } + const expiresAt = new Date( + (refreshed.data.created_at + refreshed.data.expires_in) * 1000 + ).toISOString(); + const [updated] = await tx + .update(platform_oauth_credentials) + .set({ + access_token_encrypted: accessTokenEncrypted.ciphertext, + access_token_expires_at: expiresAt, + refresh_token_encrypted: refreshTokenEncrypted.ciphertext, + credential_version: nextVersion, + last_used_at: now.toISOString(), + }) + .where( + and( + eq(platform_oauth_credentials.id, credential.id), + eq(platform_oauth_credentials.credential_version, credential.credential_version), + isNull(platform_oauth_credentials.revoked_at) + ) + ) + .returning({ id: platform_oauth_credentials.id }); + if (!updated) return { status: 'temporarily_unavailable' }; + + await tx + .update(platform_integrations) + .set({ + integration_type: 'oauth', + scopes: refreshed.data.scope.split(/\s+/).filter(Boolean), + updated_at: now.toISOString(), + }) + .where(eq(platform_integrations.id, loaded.integrationId)); + return { + status: 'available', + token: refreshed.data.access_token, + credentialVersion: nextVersion, + }; + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + } + + private async requestOAuthRefresh(input: { + instanceUrl: string; + clientId: string; + clientSecret: string; + refreshToken: string; + }): Promise { + let response: Response; + try { + response = await (this.dependencies.fetch ?? fetch)(`${input.instanceUrl}/oauth/token`, { + method: 'POST', + redirect: 'manual', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + client_id: input.clientId, + client_secret: input.clientSecret, + refresh_token: input.refreshToken, + grant_type: 'refresh_token', + }), + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + + let responseBody: unknown; + try { + responseBody = await readBoundedJson(response); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (!response.ok) { + const refreshError = OAuthRefreshErrorSchema.safeParse(responseBody); + return refreshError.success && refreshError.data.error === 'invalid_grant' + ? { status: 'invalid_grant' } + : { status: 'temporarily_unavailable' }; + } + const refreshed = OAuthRefreshResponseSchema.safeParse(responseBody); + return refreshed.success + ? { status: 'available', data: refreshed.data } + : { status: 'temporarily_unavailable' }; + } + + private decryptOAuthSecret( + credential: z.infer, + input: RefreshInput, + kind: 'access' | 'refresh' | 'oauth-client-secret' + ) { + const ciphertext = + kind === 'access' + ? credential.access_token_encrypted + : kind === 'refresh' + ? credential.refresh_token_encrypted + : credential.oauth_client_secret_encrypted; + if (!ciphertext) return Promise.resolve({ status: 'unreadable' } as const); + return this.crypto.decrypt({ + ciphertext, + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + aad: buildGitLabOAuthCredentialAad({ + credentialId: credential.id, + integrationId: credential.platform_integration_id, + providerBaseUrl: credential.provider_base_url, + owner: input.owner, + authorizedByUserId: credential.authorized_by_user_id, + credentialVersion: credential.credential_version, + kind, + }), + }); + } + + private encryptOAuthSecret( + plaintext: string, + credential: z.infer, + input: RefreshInput, + kind: 'access' | 'refresh', + credentialVersion: number + ) { + return this.crypto.encrypt({ + plaintext, + scheme: GITLAB_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + aad: buildGitLabOAuthCredentialAad({ + credentialId: credential.id, + integrationId: credential.platform_integration_id, + providerBaseUrl: credential.provider_base_url, + owner: input.owner, + authorizedByUserId: credential.authorized_by_user_id, + credentialVersion, + kind, + }), + }); + } +} diff --git a/services/git-token-service/src/gitlab-runtime-token-resolver.test.ts b/services/git-token-service/src/gitlab-runtime-token-resolver.test.ts new file mode 100644 index 0000000000..3621f61bbe --- /dev/null +++ b/services/git-token-service/src/gitlab-runtime-token-resolver.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from 'vitest'; +import { resolveGitLabRuntimeToken } from './gitlab-runtime-token-resolver.js'; + +describe('resolveGitLabRuntimeToken one-way credentials', () => { + it('resolves an OAuth integration through the unified resolver and fences only its stable ID', async () => { + const integrationId = '123e4567-e89b-12d3-a456-426614174011'; + const credentialResolver = { + resolveCredential: vi.fn().mockResolvedValue({ + status: 'available', + token: 'encrypted-oauth-token', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: true, + integrationId, + credentialId: '123e4567-e89b-12d3-a456-426614174012', + credentialVersion: 4, + source: { type: 'integration' }, + }), + }; + const lookupService = { + findGitLabIntegration: vi.fn().mockResolvedValue({ + success: true, + integrationId, + integrationType: 'oauth', + accountId: '42', + accountLogin: 'octocat', + metadata: { gitlab_instance_url: 'https://gitlab.example.com' }, + }), + findAuthorizedGitLabIntegrations: vi.fn(), + }; + + await expect( + resolveGitLabRuntimeToken({ userId: 'user-1' }, { lookupService, credentialResolver }) + ).resolves.toEqual({ + success: true, + token: 'encrypted-oauth-token', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: true, + integrationId, + source: { + type: 'integration', + credentialId: '123e4567-e89b-12d3-a456-426614174012', + }, + }); + }); +}); diff --git a/services/git-token-service/src/gitlab-runtime-token-resolver.ts b/services/git-token-service/src/gitlab-runtime-token-resolver.ts index 9ca0fe37fe..11601fb1e1 100644 --- a/services/git-token-service/src/gitlab-runtime-token-resolver.ts +++ b/services/git-token-service/src/gitlab-runtime-token-resolver.ts @@ -1,4 +1,5 @@ import * as z from 'zod'; +import type { GitLabCredentialBroker } from './gitlab-credential-broker.js'; import { isValidGitLabRepositoryUrl, matchGitLabRepositoryToIntegration, @@ -9,7 +10,6 @@ import { sha256Digest, type GitLabCapabilityCredentialSource, } from './gitlab-session-capability.js'; -import type { GitLabTokenService } from './gitlab-token-service.js'; export type GetGitLabTokenParams = { userId: string; @@ -47,12 +47,19 @@ export type GetGitLabTokenFailure = { export type GetGitLabTokenResult = GetGitLabTokenSuccess | GetGitLabTokenFailure; +type GitLabCredentialResolver = Pick & { + hasProjectCredentialCandidates?( + actor: { userId: string; orgId?: string }, + integrationId: string + ): Promise; +}; + type GitLabRuntimeTokenDependencies = { lookupService: Pick< GitLabLookupService, 'findGitLabIntegration' | 'findAuthorizedGitLabIntegrations' >; - tokenService: Pick; + credentialResolver: GitLabCredentialResolver; }; type GitLabProjectTokenCandidate = { @@ -60,6 +67,8 @@ type GitLabProjectTokenCandidate = { instanceUrl: string; integrationId: string; projectId: number; + credentialId?: string; + credentialVersion?: number; }; type GitLabCandidateEvaluation = @@ -68,9 +77,66 @@ type GitLabCandidateEvaluation = | { status: 'lookup_failed' } | { status: 'token_failed'; failure: GetGitLabTokenFailure }; -const GitLabProjectIdentitySchema = z.object({ - id: z.number().int().positive(), -}); +const GitLabProjectIdentitySchema = z.object({ id: z.number().int().positive() }).strict(); +const MAX_PROJECT_LOOKUP_RESPONSE_BYTES = 16_000; + +function mapCredentialFailure(status: string, project = false): GetGitLabTokenFailure { + return { + success: false, + reason: + status === 'temporarily_unavailable' + ? 'token_refresh_failed' + : status === 'invalid_request' + ? 'invalid_org_id' + : project + ? 'no_project_token' + : 'no_token', + }; +} + +async function readBoundedProjectIdentity(response: Response): Promise { + if (!response.body) return null; + const contentLength = response.headers.get('Content-Length'); + if ( + contentLength && + (!/^[0-9]+$/.test(contentLength) || Number(contentLength) > MAX_PROJECT_LOOKUP_RESPONSE_BYTES) + ) { + return null; + } + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + try { + while (true) { + const chunk = await reader.read(); + if (chunk.done) break; + const value: unknown = chunk.value; + if (!(value instanceof Uint8Array)) return null; + total += value.byteLength; + if (total > MAX_PROJECT_LOOKUP_RESPONSE_BYTES) { + await reader.cancel(); + return null; + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const body = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + try { + const parsed = GitLabProjectIdentitySchema.safeParse( + JSON.parse(new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(body)) + ); + return parsed.success ? parsed.data.id : null; + } catch { + return null; + } +} async function lookupGitLabProjectId( match: GitLabRepositoryMatch, @@ -79,139 +145,164 @@ async function lookupGitLabProjectId( try { const response = await fetch( `${match.instanceUrl}/api/v4/projects/${encodeURIComponent(match.projectPath)}`, - { headers: { Authorization: `Bearer ${integrationToken}` } } + { + redirect: 'manual', + headers: { Authorization: `Bearer ${integrationToken}` }, + } ); - if (!response.ok) { - return null; - } - - const parsed = GitLabProjectIdentitySchema.safeParse(await response.json()); - return parsed.success ? parsed.data.id : null; + return response.ok ? readBoundedProjectIdentity(response) : null; } catch { return null; } } async function evaluateGitLabProjectTokenCandidate( + params: GetGitLabTokenParams, match: GitLabRepositoryMatch, - tokenService: Pick + resolver: GitLabCredentialResolver ): Promise { - const projectTokens = match.metadata.project_tokens; - if (!projectTokens || Object.keys(projectTokens).length === 0) { - return { status: 'ruled_out' }; + if ( + (!match.metadata.project_tokens || Object.keys(match.metadata.project_tokens).length === 0) && + resolver.hasProjectCredentialCandidates + ) { + try { + if (!(await resolver.hasProjectCredentialCandidates(params, match.integrationId))) { + return { status: 'ruled_out' }; + } + } catch { + return { + status: 'token_failed', + failure: { success: false, reason: 'token_refresh_failed' }, + }; + } } - const integrationToken = await tokenService.getToken(match.integrationId, match.metadata); - if (!integrationToken.success) { - return { status: 'token_failed', failure: integrationToken }; + const integrationCredential = await resolver.resolveCredential(params, { + credential: 'integration', + integrationId: match.integrationId, + }); + if (integrationCredential.status !== 'available') { + return { status: 'token_failed', failure: mapCredentialFailure(integrationCredential.status) }; } + if (integrationCredential.instanceUrl !== match.instanceUrl) return { status: 'ruled_out' }; - const projectId = await lookupGitLabProjectId(match, integrationToken.token); - if (projectId === null) { - return { status: 'lookup_failed' }; - } + const projectId = await lookupGitLabProjectId(match, integrationCredential.token); + if (projectId === null) return { status: 'lookup_failed' }; - const projectToken = projectTokens[String(projectId)]; - if (!projectToken) { - return { status: 'ruled_out' }; + const projectCredential = await resolver.resolveCredential(params, { + credential: 'project-exact', + integrationId: match.integrationId, + projectId: String(projectId), + }); + if (projectCredential.status !== 'available') { + return projectCredential.status === 'not_connected' + ? { status: 'ruled_out' } + : { status: 'token_failed', failure: mapCredentialFailure(projectCredential.status, true) }; } - return { status: 'qualified', candidate: { - token: projectToken.token, - instanceUrl: match.instanceUrl, - integrationId: match.integrationId, + token: projectCredential.token, + instanceUrl: projectCredential.instanceUrl, + integrationId: projectCredential.integrationId, projectId, + ...(projectCredential.credentialId + ? { + credentialId: projectCredential.credentialId, + credentialVersion: projectCredential.credentialVersion, + } + : {}), }, }; } +function integrationSource(input: { + glabIsOAuth2: boolean; + credentialId?: string; + credentialVersion?: number; +}): GitLabCapabilityCredentialSource { + if (!input.credentialId) return { type: 'integration' }; + if (input.glabIsOAuth2) return { type: 'integration', credentialId: input.credentialId }; + if (!input.credentialVersion) throw new Error('PAT credential version is missing'); + return { + type: 'integration', + credentialId: input.credentialId, + credentialVersion: input.credentialVersion, + }; +} + export async function resolveGitLabRuntimeToken( params: GetGitLabTokenParams, dependencies: GitLabRuntimeTokenDependencies ): Promise { if (params.createdOnPlatform !== 'code-review') { const integration = await dependencies.lookupService.findGitLabIntegration(params); - if (!integration.success) { - return integration; - } - - const tokenResult = await dependencies.tokenService.getToken( - integration.integrationId, - integration.metadata - ); - if (!tokenResult.success) { - return tokenResult; - } - - return { - ...tokenResult, - glabIsOAuth2: true, + if (!integration.success) return integration; + const credential = await dependencies.credentialResolver.resolveCredential(params, { + credential: 'integration', integrationId: integration.integrationId, - source: { type: 'integration' }, + }); + if (credential.status !== 'available') return mapCredentialFailure(credential.status); + return { + success: true, + token: credential.token, + instanceUrl: credential.instanceUrl, + glabIsOAuth2: credential.glabIsOAuth2, + integrationId: credential.integrationId, + source: integrationSource(credential), }; } - if (!params.repositoryUrl) { - return { success: false, reason: 'repository_url_required' }; - } const repositoryUrl = params.repositoryUrl; + if (!repositoryUrl) return { success: false, reason: 'repository_url_required' }; if (!isValidGitLabRepositoryUrl(repositoryUrl)) { return { success: false, reason: 'invalid_repository_url' }; } - - const authorizedIntegrations = - await dependencies.lookupService.findAuthorizedGitLabIntegrations(params); - if (!authorizedIntegrations.success) { - return authorizedIntegrations; - } - - const matches = authorizedIntegrations.integrations + const authorized = await dependencies.lookupService.findAuthorizedGitLabIntegrations(params); + if (!authorized.success) return authorized; + const matches = authorized.integrations .map(integration => matchGitLabRepositoryToIntegration(repositoryUrl, integration)) .filter((match): match is GitLabRepositoryMatch => match !== null); - - if (matches.length === 0) { - return { success: false, reason: 'no_matching_integration' }; - } + if (matches.length === 0) return { success: false, reason: 'no_matching_integration' }; const evaluations = await Promise.all( - matches.map(match => evaluateGitLabProjectTokenCandidate(match, dependencies.tokenService)) + matches.map(match => + evaluateGitLabProjectTokenCandidate(params, match, dependencies.credentialResolver) + ) ); - const qualifiedCandidates = evaluations.flatMap(evaluation => + const qualified = evaluations.flatMap(evaluation => evaluation.status === 'qualified' ? [evaluation.candidate] : [] ); - - if (qualifiedCandidates.length > 1) { - return { success: false, reason: 'ambiguous_integration' }; - } - - if (qualifiedCandidates.length === 0) { + if (qualified.length > 1) return { success: false, reason: 'ambiguous_integration' }; + if (qualified.length === 0) { const tokenFailure = evaluations.find(evaluation => evaluation.status === 'token_failed'); - if (tokenFailure?.status === 'token_failed') { - return tokenFailure.failure; - } + if (tokenFailure?.status === 'token_failed') return tokenFailure.failure; } - if (evaluations.some(evaluation => evaluation.status === 'lookup_failed')) { return { success: false, reason: 'project_lookup_failed' }; } + const candidate = qualified[0]; + if (!candidate) return { success: false, reason: 'no_project_token' }; - const candidate = qualifiedCandidates[0]; - if (!candidate) { - return { success: false, reason: 'no_project_token' }; - } - + const source: GitLabCapabilityCredentialSource = + candidate.credentialId && candidate.credentialVersion + ? { + type: 'project', + projectId: candidate.projectId, + credentialId: candidate.credentialId, + credentialVersion: candidate.credentialVersion, + } + : { + type: 'project', + projectId: candidate.projectId, + tokenDigest: await sha256Digest(candidate.token), + }; return { success: true, token: candidate.token, instanceUrl: candidate.instanceUrl, glabIsOAuth2: false, integrationId: candidate.integrationId, - source: { - type: 'project', - projectId: candidate.projectId, - tokenDigest: await sha256Digest(candidate.token), - }, + source, }; } diff --git a/services/git-token-service/src/gitlab-session-capability.test.ts b/services/git-token-service/src/gitlab-session-capability.test.ts index 7aee422bad..6dca25e040 100644 --- a/services/git-token-service/src/gitlab-session-capability.test.ts +++ b/services/git-token-service/src/gitlab-session-capability.test.ts @@ -65,6 +65,42 @@ describe('GitLabSessionCapabilityCodec', () => { vi.useRealTimers(); }); + it('binds a new project capability to the encrypted credential generation', () => { + const codec = new GitLabSessionCapabilityCodec(encryptionKey); + const capability = codec.issue({ + ...claims, + source: { + type: 'project', + projectId: 42, + credentialId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145d', + credentialVersion: 3, + }, + }); + + expect(codec.decode(capability).source).toEqual({ + type: 'project', + projectId: 42, + credentialId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145d', + credentialVersion: 3, + }); + }); + + it('binds an OAuth integration capability to the stable credential ID without its refresh version', () => { + const codec = new GitLabSessionCapabilityCodec(encryptionKey); + const capability = codec.issue({ + ...claims, + source: { + type: 'integration', + credentialId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145d', + }, + }); + + expect(codec.decode(capability).source).toEqual({ + type: 'integration', + credentialId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145d', + }); + }); + it.each([ ['legacy unbound v1', 'kgl1.', 1, 2 * 60 * 60 * 1000, false], ['container-bound v2', 'kgl2.', 2, 4 * 60 * 60 * 1000, true], diff --git a/services/git-token-service/src/gitlab-session-capability.ts b/services/git-token-service/src/gitlab-session-capability.ts index a25ba3f08e..fd46f5f33c 100644 --- a/services/git-token-service/src/gitlab-session-capability.ts +++ b/services/git-token-service/src/gitlab-session-capability.ts @@ -25,8 +25,14 @@ const GitLabSessionIdentitySchema = z .strict() .refine(identity => identity.accountId !== null || identity.accountLogin !== null); const GitLabProjectTokenDigestSchema = z.string().regex(/^[a-f0-9]{64}$/); -const GitLabCapabilityCredentialSourceSchema = z.discriminatedUnion('type', [ +const GitLabCredentialIdSchema = z.object({ credentialId: z.uuid() }); +const GitLabCredentialFenceSchema = GitLabCredentialIdSchema.extend({ + credentialVersion: z.number().int().positive(), +}); +const GitLabCapabilityCredentialSourceSchema = z.union([ z.object({ type: z.literal('integration') }).strict(), + GitLabCredentialIdSchema.extend({ type: z.literal('integration') }).strict(), + GitLabCredentialFenceSchema.extend({ type: z.literal('integration') }).strict(), z .object({ type: z.literal('project'), @@ -34,6 +40,10 @@ const GitLabCapabilityCredentialSourceSchema = z.discriminatedUnion('type', [ tokenDigest: GitLabProjectTokenDigestSchema, }) .strict(), + GitLabCredentialFenceSchema.extend({ + type: z.literal('project'), + projectId: z.number().int().positive(), + }).strict(), ]); const GitLabSessionCapabilityClaimsBaseSchema = z.object({ purpose: z.literal(CAPABILITY_PURPOSE), diff --git a/services/git-token-service/src/gitlab-token-service.test.ts b/services/git-token-service/src/gitlab-token-service.test.ts index 1016ac1f2c..a49e4f2912 100644 --- a/services/git-token-service/src/gitlab-token-service.test.ts +++ b/services/git-token-service/src/gitlab-token-service.test.ts @@ -1,89 +1,35 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { GitLabTokenService } from './gitlab-token-service.js'; describe('GitLabTokenService', () => { - afterEach(() => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - it('refreshes OAuth tokens against a safe instance base path', async () => { - const fetch = vi.fn().mockResolvedValue( - Response.json({ - access_token: 'refreshed-access-token', - refresh_token: 'refreshed-refresh-token', - token_type: 'bearer', - expires_in: 3600, - created_at: 1_800_000_000, - scope: 'api', - }) - ); - vi.stubGlobal('fetch', fetch); - const service = new GitLabTokenService({ - GITLAB_CLIENT_ID: 'client-id', - GITLAB_CLIENT_SECRET: 'client-secret', - } as unknown as CloudflareEnv); - vi.spyOn(service as any, 'updateIntegrationMetadata').mockResolvedValue(undefined); + it('delegates an expired legacy OAuth token to encrypted promotion', async () => { + const promoteLegacy = vi.fn().mockResolvedValue({ + status: 'available', + token: 'refreshed-access-token', + instanceUrl: 'https://gitlab.example.com/gitlab', + }); + const service = new GitLabTokenService({}, { promoteLegacy }); await expect( - service.getToken('integration_1', { - access_token: 'expired-access-token', - refresh_token: 'refresh-token', - token_expires_at: '2020-01-01T00:00:00.000Z', - auth_type: 'oauth', - gitlab_instance_url: 'https://gitlab.example.com:8443/gitlab/', - }) + service.getToken( + 'integration-1', + { + access_token: 'expired-access-token', + refresh_token: 'refresh-token', + token_expires_at: '2020-01-01T00:00:00.000Z', + auth_type: 'oauth', + gitlab_instance_url: 'https://gitlab.example.com/gitlab', + }, + { userId: 'user-1' } + ) ).resolves.toEqual({ success: true, token: 'refreshed-access-token', - instanceUrl: 'https://gitlab.example.com:8443/gitlab', + instanceUrl: 'https://gitlab.example.com/gitlab', }); - expect(fetch).toHaveBeenCalledWith('https://gitlab.example.com:8443/gitlab/oauth/token', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - client_id: 'client-id', - client_secret: 'client-secret', - refresh_token: 'refresh-token', - grant_type: 'refresh_token', - }), + expect(promoteLegacy).toHaveBeenCalledWith({ + actor: { userId: 'user-1' }, + integrationId: 'integration-1', }); }); - - it('rejects unsafe refresh targets before sending OAuth credentials', async () => { - const fetch = vi.fn(); - vi.stubGlobal('fetch', fetch); - - await expect( - new GitLabTokenService({} as CloudflareEnv).getToken('integration_1', { - access_token: 'expired-access-token', - refresh_token: 'refresh-token', - token_expires_at: '2020-01-01T00:00:00.000Z', - auth_type: 'oauth', - gitlab_instance_url: - 'https://gitlab.example.com/gitlab?redirect=https://attacker.example.com', - }) - ).resolves.toEqual({ success: false, reason: 'invalid_instance_url' }); - expect(fetch).not.toHaveBeenCalled(); - }); - - it('does not log provider response bodies when refresh fails', async () => { - const text = vi.fn().mockResolvedValue('body includes token secret'); - vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 502, text })); - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - await expect( - new GitLabTokenService({ - GITLAB_CLIENT_ID: 'client-id', - GITLAB_CLIENT_SECRET: 'client-secret', - } as unknown as CloudflareEnv).getToken('integration_1', { - access_token: 'expired-access-token', - refresh_token: 'refresh-token', - token_expires_at: '2020-01-01T00:00:00.000Z', - auth_type: 'oauth', - }) - ).resolves.toEqual({ success: false, reason: 'token_refresh_failed' }); - expect(JSON.stringify(consoleError.mock.calls)).not.toContain('body includes token secret'); - expect(text).not.toHaveBeenCalled(); - }); }); diff --git a/services/git-token-service/src/gitlab-token-service.ts b/services/git-token-service/src/gitlab-token-service.ts index e2da484323..2231a64aa8 100644 --- a/services/git-token-service/src/gitlab-token-service.ts +++ b/services/git-token-service/src/gitlab-token-service.ts @@ -1,22 +1,11 @@ -import { getWorkerDb, type WorkerDb } from '@kilocode/db/client'; -import { platform_integrations } from '@kilocode/db/schema'; -import { eq } from 'drizzle-orm'; -import * as z from 'zod'; import { DEFAULT_GITLAB_INSTANCE_URL } from './gitlab-constants.js'; import type { GitLabIntegrationMetadata } from './gitlab-lookup-service.js'; +import type { + GitLabLegacyOAuthPromotionResult, + GitLabOAuthCredentialRefresher, +} from './gitlab-oauth-credential-refresher.js'; import { normalizeGitLabInstanceUrl } from './gitlab-url.js'; -const GitLabOAuthTokenResponseSchema = z.object({ - access_token: z.string(), - refresh_token: z.string(), - token_type: z.string(), - expires_in: z.number(), - created_at: z.number(), - scope: z.string(), -}); - -type GitLabOAuthTokenResponse = z.infer; - export type GitLabTokenSuccess = { success: true; token: string; @@ -25,141 +14,64 @@ export type GitLabTokenSuccess = { export type GitLabTokenFailure = { success: false; - reason: 'no_token' | 'token_refresh_failed' | 'token_expired_no_refresh' | 'invalid_instance_url'; + reason: + | 'no_token' + | 'token_refresh_failed' + | 'token_expired_no_refresh' + | 'invalid_instance_url' + | 'encrypted_credential_available'; }; export type GitLabTokenResult = GitLabTokenSuccess | GitLabTokenFailure; -type GitLabTokenEnv = CloudflareEnv & { - GITLAB_CLIENT_ID?: string; - GITLAB_CLIENT_SECRET?: string; -}; +const REFRESH_BUFFER_MS = 5 * 60 * 1000; function isTokenExpired(expiresAt: string | null | undefined): boolean { if (!expiresAt) return true; - const expiryTime = new Date(expiresAt).getTime(); - const bufferMs = 5 * 60 * 1000; - return Date.now() >= expiryTime - bufferMs; + return Date.now() >= new Date(expiresAt).getTime() - REFRESH_BUFFER_MS; } -function calculateTokenExpiry(createdAt: number, expiresIn: number): string { - const expiresAtMs = (createdAt + expiresIn) * 1000; - return new Date(expiresAtMs).toISOString(); +function mapPromotionResult(result: GitLabLegacyOAuthPromotionResult): GitLabTokenResult { + switch (result.status) { + case 'available': + return { success: true, token: result.token, instanceUrl: result.instanceUrl }; + case 'encrypted_credential_available': + return { success: false, reason: 'encrypted_credential_available' }; + case 'reconnect_required': + return { success: false, reason: 'token_expired_no_refresh' }; + case 'temporarily_unavailable': + return { success: false, reason: 'token_refresh_failed' }; + } } export class GitLabTokenService { - constructor(private env: GitLabTokenEnv) {} + constructor( + _env: object, + private legacyOAuthPromoter?: Pick + ) {} async getToken( integrationId: string, - metadata: GitLabIntegrationMetadata + metadata: GitLabIntegrationMetadata, + actor?: { userId: string; orgId?: string } ): Promise { const instanceUrl = normalizeGitLabInstanceUrl( metadata.gitlab_instance_url || DEFAULT_GITLAB_INSTANCE_URL ); if (!instanceUrl) return { success: false, reason: 'invalid_instance_url' }; - - if (!metadata.access_token) { - return { success: false, reason: 'no_token' }; - } - + if (!metadata.access_token) return { success: false, reason: 'no_token' }; if (metadata.auth_type === 'pat') { return { success: true, token: metadata.access_token, instanceUrl }; } - - if (metadata.token_expires_at && isTokenExpired(metadata.token_expires_at)) { - if (!metadata.refresh_token) { - return { success: false, reason: 'token_expired_no_refresh' }; - } - - const clientId = metadata.client_id || this.env.GITLAB_CLIENT_ID; - const clientSecret = metadata.client_secret || this.env.GITLAB_CLIENT_SECRET; - - if (!clientId || !clientSecret) { - console.error('GitLab OAuth credentials not configured'); - return { success: false, reason: 'token_refresh_failed' }; - } - - const refreshResult = await this.refreshToken( - metadata.refresh_token, - instanceUrl, - clientId, - clientSecret - ); - - if (!refreshResult) { - return { success: false, reason: 'token_refresh_failed' }; - } - - await this.updateIntegrationMetadata(integrationId, metadata, refreshResult); - - return { success: true, token: refreshResult.access_token, instanceUrl }; - } - - return { success: true, token: metadata.access_token, instanceUrl }; - } - - private async refreshToken( - refreshToken: string, - instanceUrl: string, - clientId: string, - clientSecret: string - ): Promise { - try { - const response = await fetch(`${instanceUrl}/oauth/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - client_id: clientId, - client_secret: clientSecret, - refresh_token: refreshToken, - grant_type: 'refresh_token', - }), - }); - - if (!response.ok) { - console.error('GitLab OAuth token refresh failed:', { status: response.status }); - return null; - } - - const parsed = GitLabOAuthTokenResponseSchema.safeParse(await response.json()); - if (!parsed.success) { - console.error('Unexpected GitLab token response shape'); - return null; - } - return parsed.data; - } catch { - console.error('GitLab OAuth token refresh request failed'); - return null; + if (!isTokenExpired(metadata.token_expires_at)) { + return { success: true, token: metadata.access_token, instanceUrl }; } - } - - private async updateIntegrationMetadata( - integrationId: string, - existingMetadata: GitLabIntegrationMetadata, - tokens: GitLabOAuthTokenResponse - ): Promise { - const db = this.getDb(); - const newExpiresAt = calculateTokenExpiry(tokens.created_at, tokens.expires_in); - - await db - .update(platform_integrations) - .set({ - metadata: { - ...existingMetadata, - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - token_expires_at: newExpiresAt, - }, - updated_at: new Date().toISOString(), - }) - .where(eq(platform_integrations.id, integrationId)); - } - - private getDb(): WorkerDb { - if (!this.env.HYPERDRIVE) { - throw new Error('Hyperdrive not configured'); + if (!metadata.refresh_token) return { success: false, reason: 'token_expired_no_refresh' }; + if (!actor || !this.legacyOAuthPromoter) { + return { success: false, reason: 'token_refresh_failed' }; } - return getWorkerDb(this.env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); + return mapPromotionResult( + await this.legacyOAuthPromoter.promoteLegacy({ actor, integrationId }) + ); } } diff --git a/services/git-token-service/src/index.test.ts b/services/git-token-service/src/index.test.ts index 0c409a485e..11c298d9fd 100644 --- a/services/git-token-service/src/index.test.ts +++ b/services/git-token-service/src/index.test.ts @@ -1,5 +1,6 @@ import { signKiloToken } from '@kilocode/worker-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type * as GitLabCredentialBrokerHandlerModule from './gitlab-credential-broker-handler.js'; import type * as GitLabLookupServiceModule from './gitlab-lookup-service.js'; const serviceMocks = vi.hoisted(() => ({ @@ -14,8 +15,11 @@ const serviceMocks = vi.hoisted(() => ({ findGitLabIntegration: vi.fn(), findAuthorizedGitLabIntegrations: vi.fn(), getGitLabToken: vi.fn(), + resolveGitLabCredential: vi.fn(), + hasGitLabProjectCredentialCandidates: vi.fn(), listBitbucketRepositories: vi.fn(), resolveBitbucketToken: vi.fn(), + runGitLabCredentialAudit: vi.fn(), })); vi.mock('cloudflare:workers', () => ({ @@ -68,15 +72,101 @@ vi.mock('./gitlab-token-service.js', () => ({ }, })); +vi.mock('./gitlab-credential-broker-handler.js', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + createGitLabCredentialBroker: () => ({ + resolveCredential: serviceMocks.resolveGitLabCredential, + hasProjectCredentialCandidates: serviceMocks.hasGitLabProjectCredentialCandidates, + }), + handleGitLabCredentialBrokerRequest: async ( + _env: CloudflareEnv, + actor: { userId: string; orgId?: string }, + selector: { credential: 'integration' | 'project-exact'; integrationId: string } + ) => { + const result = await serviceMocks.resolveGitLabCredential(actor, selector); + if (result.status !== 'available') return result; + return { + status: 'available', + token: result.token, + instanceUrl: result.instanceUrl, + glabIsOAuth2: result.glabIsOAuth2, + }; + }, + }; +}); + 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'; +vi.mock('./gitlab-credential-audit-handler.js', () => ({ + runGitLabCredentialAudit: serviceMocks.runGitLabCredentialAudit, +})); + import gitTokenServiceWorker, { GitTokenRPCEntrypoint } from './index.js'; +beforeEach(() => { + serviceMocks.hasGitLabProjectCredentialCandidates.mockReset().mockResolvedValue(false); + serviceMocks.resolveGitLabCredential.mockReset().mockImplementation(async (actor, selector) => { + const latestIntegrationLookup = serviceMocks.findGitLabIntegration.mock.results.at(-1)?.value; + const latestAuthorizedLookup = + serviceMocks.findAuthorizedGitLabIntegrations.mock.results.at(-1)?.value; + let integration = latestIntegrationLookup ? await latestIntegrationLookup : undefined; + if (!integration?.success && latestAuthorizedLookup) { + const authorized = await latestAuthorizedLookup; + integration = authorized.success + ? authorized.integrations.find( + (candidate: { integrationId: string }) => + candidate.integrationId === selector.integrationId + ) + : undefined; + } + if (!integration?.success && !integration?.integrationId) { + integration = await serviceMocks.findGitLabIntegration(actor, selector.integrationId); + } + if (!integration?.success && !integration?.integrationId) { + return { status: 'not_connected' }; + } + const metadata = integration.metadata ?? {}; + const instanceUrl = metadata.gitlab_instance_url ?? 'https://gitlab.com'; + if (selector.credential === 'project-exact') { + const token = metadata.project_tokens?.[selector.projectId]?.token; + return token + ? { + status: 'available', + token, + instanceUrl, + glabIsOAuth2: false, + integrationId: integration.integrationId, + source: { type: 'project', projectId: selector.projectId }, + } + : { status: 'reconnect_required' }; + } + const token = await serviceMocks.getGitLabToken(selector.integrationId, metadata, actor); + if (!token.success) { + return { + status: + token.reason === 'token_refresh_failed' + ? 'temporarily_unavailable' + : 'reconnect_required', + }; + } + return { + status: 'available', + token: token.token, + instanceUrl: token.instanceUrl, + glabIsOAuth2: + integration.integrationType === 'oauth' || + (integration.integrationType !== 'pat' && metadata.auth_type === 'oauth'), + integrationId: integration.integrationId, + source: { type: 'integration' }, + }; + }); +}); + describe('Bitbucket repository-list HTTP authorization', () => { const jwtSecret = 'test-secret-that-is-at-least-32-characters'; const env = { NEXTAUTH_SECRET: jwtSecret } as CloudflareEnv; @@ -158,35 +248,222 @@ describe('Bitbucket repository-list HTTP authorization', () => { }); }); -const integration: AuthorizedGitLabIntegration = { - integrationId: '123e4567-e89b-12d3-a456-426614174011', - integrationType: 'oauth', - accountId: '42', - accountLogin: 'octocat', - metadata: { - access_token: 'human-integration-token', - gitlab_instance_url: 'https://gitlab.example.com/gitlab', - project_tokens: { '42': { token: 'project-bot-token' } }, - }, -}; +describe('GitLab credential broker HTTP authorization', () => { + const jwtSecret = 'test-secret-that-is-at-least-32-characters'; + const integrationId = '123e4567-e89b-12d3-a456-426614174012'; -function createDependencies(options: { integrations?: AuthorizedGitLabIntegration[] } = {}) { - const lookupService = { - findGitLabIntegration: vi.fn().mockResolvedValue({ success: true, ...integration }), - findAuthorizedGitLabIntegrations: vi.fn().mockResolvedValue({ + beforeEach(() => { + serviceMocks.findGitLabIntegration.mockReset().mockResolvedValue({ success: true, - integrations: options.integrations ?? [integration], - }), - }; - const tokenService = { - getToken: vi.fn().mockResolvedValue({ + integrationId, + integrationType: 'pat', + accountId: '42', + accountLogin: 'octocat', + metadata: { + access_token: 'legacy-gitlab-token', + auth_type: 'pat', + gitlab_instance_url: 'https://gitlab.example.com', + }, + }); + serviceMocks.getGitLabToken.mockReset().mockResolvedValue({ success: true, - token: 'human-integration-token', - instanceUrl: 'https://gitlab.example.com/gitlab', - }), - }; - return { lookupService, tokenService }; -} + token: 'legacy-gitlab-token', + instanceUrl: 'https://gitlab.example.com', + }); + }); + + it('derives the actor from a purpose-bound JWT and disables response caching', async () => { + const { token } = await signKiloToken({ + userId: 'user-1', + pepper: null, + secret: jwtSecret, + expiresInSeconds: 5 * 60, + audience: 'git-token-service:gitlab-credentials', + }); + const response = await gitTokenServiceWorker.fetch( + new Request('https://git-token-service.test/internal/gitlab/credentials', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ credential: 'integration', integrationId }), + }), + { + NEXTAUTH_SECRET: jwtSecret, + } as unknown as CloudflareEnv + ); + + expect(response.status).toBe(200); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + await expect(response.json()).resolves.toEqual({ + status: 'available', + token: 'legacy-gitlab-token', + instanceUrl: 'https://gitlab.example.com', + glabIsOAuth2: false, + }); + expect(serviceMocks.findGitLabIntegration).toHaveBeenCalledWith( + { userId: 'user-1' }, + integrationId + ); + }); + + it('rejects a token without the GitLab broker audience', async () => { + const { token } = await signKiloToken({ + userId: 'user-1', + pepper: null, + secret: jwtSecret, + expiresInSeconds: 5 * 60, + }); + const response = await gitTokenServiceWorker.fetch( + new Request('https://git-token-service.test/internal/gitlab/credentials', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ credential: 'integration', integrationId }), + }), + { + NEXTAUTH_SECRET: jwtSecret, + } as unknown as CloudflareEnv + ); + + expect(response.status).toBe(401); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + expect(serviceMocks.findGitLabIntegration).not.toHaveBeenCalled(); + }); +}); + +describe('GitLab credential audit HTTP authorization', () => { + const jwtSecret = 'test-secret-that-is-at-least-32-characters'; + const path = 'https://git-token-service.test/internal/gitlab/credential-audit'; + + beforeEach(() => { + serviceMocks.runGitLabCredentialAudit.mockReset().mockResolvedValue({ + authorized: true, + result: { + activeKey: { keyId: 'active', publicKeySha256: 'a'.repeat(64) }, + counts: { + credentials: 1, + secrets: 1, + passedCredentials: 1, + profileFailures: 0, + configurationFailures: 0, + parseFailures: 0, + unknownKeyFailures: 0, + decryptOrAadFailures: 0, + }, + failingCredentials: { + profile: [], + configuration: [], + parse: [], + unknownKey: [], + decryptOrAad: [], + }, + nextCursor: null, + }, + }); + }); + + async function authorization(audience?: string) { + return signKiloToken({ + userId: 'admin-1', + pepper: null, + secret: jwtSecret, + expiresInSeconds: 5 * 60, + ...(audience ? { audience } : {}), + }); + } + + it('requires its dedicated audience and returns only the no-store audit result', async () => { + const { token } = await authorization('git-token-service:gitlab-credential-audit'); + const response = await gitTokenServiceWorker.fetch( + new Request(path, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ limit: 10 }), + }), + { NEXTAUTH_SECRET: jwtSecret } as CloudflareEnv + ); + + expect(response.status).toBe(200); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + const result = await response.json(); + expect(result).toEqual(expect.objectContaining({ nextCursor: null })); + expect(result).toEqual( + expect.objectContaining({ + activeKey: { keyId: 'active', publicKeySha256: 'a'.repeat(64) }, + }) + ); + expect(JSON.stringify(result)).not.toMatch(/plaintext|ciphertext|token_encrypted/i); + expect(serviceMocks.runGitLabCredentialAudit).toHaveBeenCalledWith( + expect.anything(), + 'admin-1', + { limit: 10 } + ); + }); + + it('rejects a token without the audit audience before the admin lookup', async () => { + const { token } = await authorization(); + const response = await gitTokenServiceWorker.fetch( + new Request(path, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: '{}', + }), + { NEXTAUTH_SECRET: jwtSecret } as CloudflareEnv + ); + + expect(response.status).toBe(401); + expect(serviceMocks.runGitLabCredentialAudit).not.toHaveBeenCalled(); + }); + + it('rejects a non-admin after the database-backed authorization check', async () => { + serviceMocks.runGitLabCredentialAudit.mockResolvedValue({ authorized: false }); + const { token } = await authorization('git-token-service:gitlab-credential-audit'); + const response = await gitTokenServiceWorker.fetch( + new Request(path, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: '{}', + }), + { NEXTAUTH_SECRET: jwtSecret } as CloudflareEnv + ); + + expect(response.status).toBe(403); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + await expect(response.json()).resolves.toEqual({ error: 'forbidden' }); + }); + + it('rejects unknown or oversized request fields before database access', async () => { + const { token } = await authorization('git-token-service:gitlab-credential-audit'); + const response = await gitTokenServiceWorker.fetch( + new Request(path, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ limit: 101, unknown: true }), + }), + { NEXTAUTH_SECRET: jwtSecret } as CloudflareEnv + ); + + expect(response.status).toBe(400); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + expect(serviceMocks.runGitLabCredentialAudit).not.toHaveBeenCalled(); + }); +}); function createService(): GitTokenRPCEntrypoint { return new GitTokenRPCEntrypoint( @@ -244,288 +521,6 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe('resolveGitLabRuntimeToken', () => { - it('preserves ordinary integration token behavior and OAuth CLI mode', async () => { - const dependencies = createDependencies(); - - await expect(resolveGitLabRuntimeToken({ userId: 'user_123' }, dependencies)).resolves.toEqual({ - success: true, - token: 'human-integration-token', - instanceUrl: 'https://gitlab.example.com/gitlab', - integrationId: integration.integrationId, - source: { type: 'integration' }, - glabIsOAuth2: true, - }); - expect(dependencies.lookupService.findGitLabIntegration).toHaveBeenCalledWith({ - userId: 'user_123', - }); - expect(dependencies.lookupService.findAuthorizedGitLabIntegrations).not.toHaveBeenCalled(); - expect(dependencies.tokenService.getToken).toHaveBeenCalledOnce(); - }); - - it('returns the stored project token for an exact review-origin repository match', async () => { - const fetchMock = vi.fn().mockResolvedValue(Response.json({ id: 42 })); - vi.stubGlobal('fetch', fetchMock); - const dependencies = createDependencies(); - - await expect( - resolveGitLabRuntimeToken( - { - userId: 'user_123', - repositoryUrl: 'https://gitlab.example.com/gitlab/team/repo.git', - createdOnPlatform: 'code-review', - }, - dependencies - ) - ).resolves.toEqual({ - success: true, - token: 'project-bot-token', - instanceUrl: 'https://gitlab.example.com/gitlab', - integrationId: integration.integrationId, - source: { - type: 'project', - projectId: 42, - tokenDigest: '3f4dff81e5f3e75d64343bfe237db23397715d8fbccbb1e035fb20a6d15f4603', - }, - glabIsOAuth2: false, - }); - expect(dependencies.tokenService.getToken).toHaveBeenCalledWith( - integration.integrationId, - integration.metadata - ); - expect(fetchMock).toHaveBeenCalledWith( - 'https://gitlab.example.com/gitlab/api/v4/projects/team%2Frepo', - { headers: { Authorization: 'Bearer human-integration-token' } } - ); - }); - - it('fails closed when review-origin repository context is missing or malformed', async () => { - const dependencies = createDependencies(); - - await expect( - resolveGitLabRuntimeToken( - { userId: 'user_123', createdOnPlatform: 'code-review' }, - dependencies - ) - ).resolves.toEqual({ success: false, reason: 'repository_url_required' }); - await expect( - resolveGitLabRuntimeToken( - { - userId: 'user_123', - repositoryUrl: 'not-a-url', - createdOnPlatform: 'code-review', - }, - dependencies - ) - ).resolves.toEqual({ success: false, reason: 'invalid_repository_url' }); - expect(dependencies.lookupService.findAuthorizedGitLabIntegrations).not.toHaveBeenCalled(); - expect(dependencies.tokenService.getToken).not.toHaveBeenCalled(); - }); - - it('fails closed for unmatched authorized instance candidates', async () => { - const unmatched = createDependencies({ - integrations: [ - { - ...integration, - metadata: { ...integration.metadata, gitlab_instance_url: 'https://other.example.com' }, - }, - ], - }); - await expect( - resolveGitLabRuntimeToken( - { - userId: 'user_123', - repositoryUrl: 'https://gitlab.example.com/gitlab/team/repo.git', - createdOnPlatform: 'code-review', - }, - unmatched - ) - ).resolves.toEqual({ success: false, reason: 'no_matching_integration' }); - expect(unmatched.tokenService.getToken).not.toHaveBeenCalled(); - }); - - it('returns the unique project token when multiple integrations match but only one owns the project', async () => { - const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(Response.json({ id: 42 }))); - vi.stubGlobal('fetch', fetchMock); - const integrationWithoutProjectToken: AuthorizedGitLabIntegration = { - ...integration, - integrationId: 'another-integration', - metadata: { - ...integration.metadata, - project_tokens: { '99': { token: 'other-project-token' } }, - }, - }; - const dependencies = createDependencies({ - integrations: [integrationWithoutProjectToken, integration], - }); - - await expect( - resolveGitLabRuntimeToken( - { - userId: 'user_123', - repositoryUrl: 'https://gitlab.example.com/gitlab/team/repo.git', - createdOnPlatform: 'code-review', - }, - dependencies - ) - ).resolves.toEqual({ - success: true, - token: 'project-bot-token', - instanceUrl: 'https://gitlab.example.com/gitlab', - integrationId: integration.integrationId, - source: { - type: 'project', - projectId: 42, - tokenDigest: '3f4dff81e5f3e75d64343bfe237db23397715d8fbccbb1e035fb20a6d15f4603', - }, - glabIsOAuth2: false, - }); - expect(dependencies.tokenService.getToken).toHaveBeenCalledTimes(2); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); - - it('uses a matched project token when another matching integration token fails', async () => { - vi.stubGlobal( - 'fetch', - vi.fn().mockImplementation(() => Promise.resolve(Response.json({ id: 42 }))) - ); - const failingIntegration: AuthorizedGitLabIntegration = { - ...integration, - integrationId: 'failing-integration', - metadata: { - ...integration.metadata, - project_tokens: { '42': { token: 'failing-project-token' } }, - }, - }; - const dependencies = createDependencies({ - integrations: [failingIntegration, integration], - }); - dependencies.tokenService.getToken.mockImplementation(integrationId => - integrationId === failingIntegration.integrationId - ? Promise.resolve({ success: false, reason: 'token_expired_no_refresh' }) - : Promise.resolve({ - success: true, - token: 'human-integration-token', - instanceUrl: 'https://gitlab.example.com/gitlab', - }) - ); - - await expect( - resolveGitLabRuntimeToken( - { - userId: 'user_123', - repositoryUrl: 'https://gitlab.example.com/gitlab/team/repo.git', - createdOnPlatform: 'code-review', - }, - dependencies - ) - ).resolves.toEqual({ - success: true, - token: 'project-bot-token', - instanceUrl: 'https://gitlab.example.com/gitlab', - integrationId: integration.integrationId, - source: { - type: 'project', - projectId: 42, - tokenDigest: '3f4dff81e5f3e75d64343bfe237db23397715d8fbccbb1e035fb20a6d15f4603', - }, - glabIsOAuth2: false, - }); - }); - - it('skips project lookup for matching integrations without stored project tokens', async () => { - const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(Response.json({ id: 42 }))); - vi.stubGlobal('fetch', fetchMock); - const integrationWithoutProjectTokens: AuthorizedGitLabIntegration = { - ...integration, - integrationId: 'another-integration', - metadata: { - access_token: integration.metadata.access_token, - gitlab_instance_url: integration.metadata.gitlab_instance_url, - }, - }; - const dependencies = createDependencies({ - integrations: [integrationWithoutProjectTokens, integration], - }); - - await expect( - resolveGitLabRuntimeToken( - { - userId: 'user_123', - repositoryUrl: 'https://gitlab.example.com/gitlab/team/repo.git', - createdOnPlatform: 'code-review', - }, - dependencies - ) - ).resolves.toEqual({ - success: true, - token: 'project-bot-token', - instanceUrl: 'https://gitlab.example.com/gitlab', - integrationId: integration.integrationId, - source: { - type: 'project', - projectId: 42, - tokenDigest: '3f4dff81e5f3e75d64343bfe237db23397715d8fbccbb1e035fb20a6d15f4603', - }, - glabIsOAuth2: false, - }); - expect(dependencies.tokenService.getToken).toHaveBeenCalledOnce(); - expect(fetchMock).toHaveBeenCalledOnce(); - }); - - it('fails closed when multiple matching integrations own the resolved project token', async () => { - vi.stubGlobal( - 'fetch', - vi.fn().mockImplementation(() => Promise.resolve(Response.json({ id: 42 }))) - ); - const ambiguous = createDependencies({ - integrations: [ - integration, - { - ...integration, - integrationId: 'another-integration', - metadata: { - ...integration.metadata, - project_tokens: { '42': { token: 'duplicate-project-bot-token' } }, - }, - }, - ], - }); - await expect( - resolveGitLabRuntimeToken( - { - userId: 'user_123', - repositoryUrl: 'https://gitlab.example.com/gitlab/team/repo.git', - createdOnPlatform: 'code-review', - }, - ambiguous - ) - ).resolves.toEqual({ success: false, reason: 'ambiguous_integration' }); - expect(ambiguous.tokenService.getToken).toHaveBeenCalledTimes(2); - }); - - it('does not fall back to the integration token when project resolution or storage fails', async () => { - vi.stubGlobal('fetch', vi.fn().mockResolvedValue(Response.json({ id: 99 }))); - const dependencies = createDependencies(); - const reviewContext = { - userId: 'user_123', - repositoryUrl: 'https://gitlab.example.com/gitlab/team/repo.git', - createdOnPlatform: 'code-review', - }; - - await expect(resolveGitLabRuntimeToken(reviewContext, dependencies)).resolves.toEqual({ - success: false, - reason: 'no_project_token', - }); - - vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 404 }))); - await expect(resolveGitLabRuntimeToken(reviewContext, dependencies)).resolves.toEqual({ - success: false, - reason: 'project_lookup_failed', - }); - }); -}); - describe('GitTokenRPCEntrypoint.getTokenForRepo', () => { beforeEach(() => { vi.clearAllMocks(); @@ -1631,6 +1626,71 @@ describe('GitTokenRPCEntrypoint GitLab session capability RPCs', () => { expect(serviceMocks.getGitLabToken).not.toHaveBeenCalled(); }); + it('keeps OAuth capabilities valid across refresh and rejects credential replacement', async () => { + const getCredential = vi + .fn() + .mockResolvedValueOnce({ + status: 'available', + token: 'issued-encrypted-token', + instanceUrl: 'https://gitlab.com', + integrationId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145c', + glabIsOAuth2: true, + credentialId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145d', + credentialVersion: 4, + source: { type: 'integration' }, + }) + .mockResolvedValueOnce({ + status: 'available', + token: 'refreshed-same-generation-token', + instanceUrl: 'https://gitlab.com', + integrationId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145c', + glabIsOAuth2: true, + credentialId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145d', + credentialVersion: 3, + source: { type: 'integration' }, + }) + .mockResolvedValueOnce({ + status: 'available', + token: 'rotated-encrypted-token', + instanceUrl: 'https://gitlab.com', + integrationId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145c', + glabIsOAuth2: true, + credentialId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145e', + credentialVersion: 1, + source: { type: 'integration' }, + }); + const service = createService(); + Object.assign(service, { + gitlabCredentialResolver: { + resolveCredential: getCredential, + hasProjectCredentialCandidates: vi.fn().mockResolvedValue(false), + }, + }); + const issued = await service.issueGitLabSessionCapability({ + gitUrl: 'https://gitlab.com/acme/widgets.git', + userId: 'user_1', + outboundContainerId, + }); + if (!issued.success) throw new Error('Expected successful issuance'); + + const redemption = { + capability: issued.capability, + outboundContainerId, + requestMethod: 'GET', + requestUrl: 'https://gitlab.com/api/v4/projects/acme%2Fwidgets/issues', + }; + await expect(service.redeemGitLabSessionCapability(redemption)).resolves.toEqual({ + success: true, + headers: { authorization: 'Bearer refreshed-same-generation-token' }, + }); + await expect( + service.redeemGitLabSessionCapability({ + ...redemption, + }) + ).resolves.toEqual({ success: false, reason: 'source_unavailable' }); + expect(serviceMocks.getGitLabToken).not.toHaveBeenCalled(); + }); + it('temporarily issues and redeems a legacy unbound GitLab capability for an old caller', async () => { const service = createService(); const issued = await service.issueGitLabSessionCapability({ @@ -1720,7 +1780,7 @@ describe('GitTokenRPCEntrypoint GitLab session capability RPCs', () => { }, ], }); - serviceMocks.findGitLabIntegration.mockResolvedValueOnce({ + serviceMocks.findGitLabIntegration.mockResolvedValue({ success: true, integrationId: 'ef2eb5c7-27ce-4f43-b6d3-8f282abc145c', integrationType: 'oauth', @@ -1848,7 +1908,7 @@ describe('GitTokenRPCEntrypoint GitLab session capability RPCs', () => { success: true, integrations: [projectIntegration], }); - serviceMocks.findGitLabIntegration.mockResolvedValueOnce(projectIntegration); + serviceMocks.findGitLabIntegration.mockResolvedValue(projectIntegration); const service = createService(); const issued = await service.issueGitLabSessionCapability({ gitUrl: 'https://gitlab.com/acme/widgets.git', diff --git a/services/git-token-service/src/index.ts b/services/git-token-service/src/index.ts index d553f54bc0..681779ada1 100644 --- a/services/git-token-service/src/index.ts +++ b/services/git-token-service/src/index.ts @@ -8,6 +8,8 @@ import { BITBUCKET_CODE_REVIEW_PULL_REQUEST_AUDIENCE, BITBUCKET_CODE_REVIEW_WEBHOOK_DELETE_AUDIENCE, BITBUCKET_CODE_REVIEW_WEBHOOK_ENSURE_AUDIENCE, + GITLAB_CREDENTIAL_AUDIT_AUDIENCE, + GITLAB_CREDENTIAL_BROKER_AUDIENCE, } from '@kilocode/worker-utils/internal-service-token-audiences'; import { WorkerEntrypoint } from 'cloudflare:workers'; import { GitHubTokenService, type GitHubAppType } from './github-token-service.js'; @@ -34,7 +36,14 @@ import { parseGitLabCloneUrl, type GitLabCloneUrlFailureReason, } from './gitlab-url.js'; -import { GitLabTokenService } from './gitlab-token-service.js'; +import { + GitLabCredentialBrokerRequestSchema, + createGitLabCredentialBroker, + handleGitLabCredentialBrokerRequest, +} from './gitlab-credential-broker-handler.js'; +import { GitLabCredentialAuditRequestSchema } from './gitlab-credential-audit.js'; +import { runGitLabCredentialAudit } from './gitlab-credential-audit-handler.js'; +import type { GitLabCredentialBroker } from './gitlab-credential-broker.js'; import { InstallationLookupService } from './installation-lookup-service.js'; import { GitHubSessionCapabilityCodec, @@ -245,6 +254,8 @@ const BITBUCKET_REPOSITORIES_PATH = '/internal/bitbucket/repositories'; const BITBUCKET_CODE_REVIEW_PULL_REQUEST_PATH = '/internal/bitbucket/code-review/pull-request'; const BITBUCKET_CODE_REVIEW_WEBHOOK_ENSURE_PATH = '/internal/bitbucket/code-review/webhooks/ensure'; const BITBUCKET_CODE_REVIEW_WEBHOOK_DELETE_PATH = '/internal/bitbucket/code-review/webhooks/delete'; +const GITLAB_CREDENTIAL_BROKER_PATH = '/internal/gitlab/credentials'; +const GITLAB_CREDENTIAL_AUDIT_PATH = '/internal/gitlab/credential-audit'; const INTERNAL_REQUEST_MAX_BYTES = 16_000; const BitbucketPullRequestHttpRequestSchema = BitbucketPullRequestRequestSchema.omit({ @@ -578,7 +589,7 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { private githubService: GitHubTokenService; private installationLookupService: InstallationLookupService; private gitlabLookupService: GitLabLookupService; - private gitlabTokenService: GitLabTokenService; + private gitlabCredentialResolver: GitLabCredentialBroker; private githubUserAuthorizationService: GitHubUserAuthorizationService; constructor(ctx: ExecutionContext, env: CloudflareEnv) { @@ -586,7 +597,7 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { this.githubService = new GitHubTokenService(env); this.installationLookupService = new InstallationLookupService(env); this.gitlabLookupService = new GitLabLookupService(env); - this.gitlabTokenService = new GitLabTokenService(env); + this.gitlabCredentialResolver = createGitLabCredentialBroker(env); this.githubUserAuthorizationService = new GitHubUserAuthorizationService(env); } @@ -928,7 +939,7 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { async getGitLabToken(params: GetGitLabTokenParams): Promise { return resolveGitLabRuntimeToken(params, { lookupService: this.gitlabLookupService, - tokenService: this.gitlabTokenService, + credentialResolver: this.gitlabCredentialResolver, }); } @@ -945,7 +956,7 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { { ...params, repositoryUrl: params.gitUrl }, { lookupService: this.gitlabLookupService, - tokenService: this.gitlabTokenService, + credentialResolver: this.gitlabCredentialResolver, } ); if (!runtimeToken.success) return runtimeToken; @@ -1045,23 +1056,31 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { return { success: false, reason: 'identity_mismatch' }; } - let token: string; - if (claims.source.type === 'integration') { - const integrationToken = await this.gitlabTokenService.getToken( - integration.integrationId, - integration.metadata - ); - if (!integrationToken.success) return { success: false, reason: 'source_unavailable' }; - token = integrationToken.token; - } else { - const projectToken = integration.metadata.project_tokens?.[String(claims.source.projectId)]; - if (!projectToken) return { success: false, reason: 'source_unavailable' }; - const currentTokenDigest = await sha256Digest(projectToken.token); + const selector = + claims.source.type === 'integration' + ? ({ credential: 'integration', integrationId: claims.integrationId } as const) + : ({ + credential: 'project-exact', + integrationId: claims.integrationId, + projectId: String(claims.source.projectId), + } as const); + const credential = await this.gitlabCredentialResolver.resolveCredential(context, selector); + if ( + credential.status !== 'available' || + credential.source.type !== claims.source.type || + ('credentialId' in claims.source && credential.credentialId !== claims.source.credentialId) || + ('credentialVersion' in claims.source && + credential.credentialVersion !== claims.source.credentialVersion) + ) { + return { success: false, reason: 'source_unavailable' }; + } + if ('tokenDigest' in claims.source) { + const currentTokenDigest = await sha256Digest(credential.token); if (!timingSafeEqual(currentTokenDigest, claims.source.tokenDigest)) { return { success: false, reason: 'source_unavailable' }; } - token = projectToken.token; } + const token = credential.token; if (upstream.authSurface === 'git') { return { @@ -1133,11 +1152,10 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { } private getGitLabAuthType(integration: GitLabLookupSuccess): GitLabAuthType | null { - if (integration.metadata.auth_type) return integration.metadata.auth_type; if (integration.integrationType === 'oauth' || integration.integrationType === 'pat') { return integration.integrationType; } - return null; + return integration.metadata.auth_type ?? null; } private getGitLabSessionIdentity(integration: GitLabLookupSuccess): GitLabSessionIdentity | null { @@ -1149,36 +1167,100 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { export default { async fetch(request: Request, env: ServiceHttpEnv): Promise { const url = new URL(request.url); + const isGitLabCredentialAudit = url.pathname === GITLAB_CREDENTIAL_AUDIT_PATH; + const isGitLabCredentialBroker = url.pathname === GITLAB_CREDENTIAL_BROKER_PATH; + const privateGitLabHeaders = + isGitLabCredentialAudit || isGitLabCredentialBroker + ? { 'Cache-Control': 'no-store' } + : undefined; const codeReviewAudience = bitbucketCodeReviewAudiences.get(url.pathname); if ( url.pathname !== DISCONNECT_PATH && url.pathname !== BITBUCKET_REPOSITORIES_PATH && + url.pathname !== GITLAB_CREDENTIAL_BROKER_PATH && + !isGitLabCredentialAudit && !codeReviewAudience ) { return new Response(null, { status: 404 }); } - if (request.method !== 'POST') return new Response(null, { status: 405 }); + if (request.method !== 'POST') { + return new Response(null, { status: 405, headers: privateGitLabHeaders }); + } const token = extractBearerToken(request.headers.get('Authorization')); - if (!token) return Response.json({ error: 'unauthorized' }, { status: 401 }); + if (!token) { + return Response.json( + { error: 'unauthorized' }, + { status: 401, headers: privateGitLabHeaders } + ); + } let secret: string; try { secret = await resolveSecret(env.NEXTAUTH_SECRET); } catch { - return Response.json({ error: 'authentication_unavailable' }, { status: 503 }); + return Response.json( + { error: 'authentication_unavailable' }, + { status: 503, headers: privateGitLabHeaders } + ); + } + if (!secret) { + return Response.json( + { error: 'authentication_unavailable' }, + { status: 503, headers: privateGitLabHeaders } + ); } - if (!secret) return Response.json({ error: 'authentication_unavailable' }, { status: 503 }); let authorization: Awaited>; try { const audience = url.pathname === BITBUCKET_REPOSITORIES_PATH ? BITBUCKET_REPOSITORY_LIST_AUDIENCE - : codeReviewAudience; + : url.pathname === GITLAB_CREDENTIAL_BROKER_PATH + ? GITLAB_CREDENTIAL_BROKER_AUDIENCE + : isGitLabCredentialAudit + ? GITLAB_CREDENTIAL_AUDIT_AUDIENCE + : codeReviewAudience; authorization = await verifyKiloToken(token, secret, audience ? { audience } : undefined); } catch { - return Response.json({ error: 'unauthorized' }, { status: 401 }); + return Response.json( + { error: 'unauthorized' }, + { status: 401, headers: privateGitLabHeaders } + ); + } + + if (isGitLabCredentialAudit) { + let body: unknown; + try { + body = await readBoundedInternalJsonRequest(request); + } catch { + return Response.json( + { error: 'invalid_request' }, + { status: 400, headers: { 'Cache-Control': 'no-store' } } + ); + } + const parsed = GitLabCredentialAuditRequestSchema.safeParse(body); + if (!parsed.success) { + return Response.json( + { error: 'invalid_request' }, + { status: 400, headers: { 'Cache-Control': 'no-store' } } + ); + } + try { + const audit = await runGitLabCredentialAudit(env, authorization.kiloUserId, parsed.data); + if (!audit.authorized) { + return Response.json( + { error: 'forbidden' }, + { status: 403, headers: { 'Cache-Control': 'no-store' } } + ); + } + return Response.json(audit.result, { headers: { 'Cache-Control': 'no-store' } }); + } catch { + return Response.json( + { error: 'temporarily_unavailable' }, + { status: 503, headers: { 'Cache-Control': 'no-store' } } + ); + } } if (url.pathname === BITBUCKET_REPOSITORIES_PATH) { @@ -1196,6 +1278,41 @@ export default { } } + if (url.pathname === GITLAB_CREDENTIAL_BROKER_PATH) { + let body: unknown; + try { + body = await readBoundedInternalJsonRequest(request); + } catch { + return Response.json( + { status: 'invalid_request' }, + { status: 400, headers: { 'Cache-Control': 'no-store' } } + ); + } + const parsed = GitLabCredentialBrokerRequestSchema.safeParse(body); + if (!parsed.success) { + return Response.json( + { status: 'invalid_request' }, + { status: 400, headers: { 'Cache-Control': 'no-store' } } + ); + } + try { + const result = await handleGitLabCredentialBrokerRequest( + env, + { + userId: authorization.kiloUserId, + ...(authorization.organizationId ? { orgId: authorization.organizationId } : {}), + }, + parsed.data + ); + return Response.json(result, { headers: { 'Cache-Control': 'no-store' } }); + } catch { + return Response.json( + { status: 'temporarily_unavailable' }, + { headers: { 'Cache-Control': 'no-store' } } + ); + } + } + if (codeReviewAudience) { if (!authorization.organizationId) { return Response.json({ error: 'organization_required' }, { status: 403 }); diff --git a/services/git-token-service/worker-configuration.d.ts b/services/git-token-service/worker-configuration.d.ts index 72c7db642f..990092c078 100644 --- a/services/git-token-service/worker-configuration.d.ts +++ b/services/git-token-service/worker-configuration.d.ts @@ -1,14 +1,14 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv worker-configuration.d.ts` (hash: 7449dd875d4d2143655db8497181d8bd) +// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv worker-configuration.d.ts` (hash: 8b602780e3fc9b5bfe915e87627165c2) // 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; + BITBUCKET_CLIENT_ID: SecretsStoreSecret; + BITBUCKET_CLIENT_SECRET: SecretsStoreSecret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: SecretsStoreSecret; GITHUB_APP_SLUG: "kiloconnect-development" | "kiloconnect"; GITHUB_APP_BOT_USER_ID: "242397087" | "240665456"; GITHUB_LITE_APP_SLUG: "" | "kiloconnect-lite"; @@ -21,8 +21,6 @@ interface __BaseEnv_CloudflareEnv { 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 { @@ -49,11 +47,6 @@ declare namespace Cloudflare { GITHUB_APP_CLIENT_ID: string; GITHUB_APP_CLIENT_SECRET: string; NEXTAUTH_SECRET: string; - BITBUCKET_CLIENT_ID: string; - BITBUCKET_CLIENT_SECRET: string; - BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: string; - BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: string; - BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: string; } interface Env extends __BaseEnv_CloudflareEnv {} } @@ -62,7 +55,7 @@ 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