import { query } from './index.js'; import { decryptSecret, encryptSecret } from '../lib/crypto.js'; import type { IntegrationRow, ProviderSlug } from '../types/integration.js'; type IntegrationRecord = { id: number; provider: ProviderSlug; external_workspace_id: string | null; display_name: string | null; api_key_hash: string | null; access_token_ciphertext: string | null; refresh_token_ciphertext: string | null; signing_secret_ciphertext: string | null; token_expires_at: Date | null; scopes: string[] | null; }; const PUBLIC_COLUMNS = ` id, provider, external_workspace_id, display_name, token_expires_at, scopes `; /** * Ciphertext columns are dropped here rather than in the query so that every * caller path converges on a value that structurally cannot carry a secret. */ const toPublicRow = (record: IntegrationRecord): IntegrationRow => ({ id: record.id, provider: record.provider, externalWorkspaceId: record.external_workspace_id, displayName: record.display_name, scopes: record.scopes ?? [], tokenExpiresAt: record.token_expires_at, }); export const findByApiKeyHash = async ( hash: string ): Promise => { const { rows } = await query( `SELECT ${PUBLIC_COLUMNS} FROM integrations WHERE api_key_hash = $1`, [hash] ); return rows[0] ? toPublicRow(rows[0]) : null; }; export const findByProviderWorkspace = async ( provider: ProviderSlug, externalWorkspaceId: string ): Promise => { const { rows } = await query( `SELECT ${PUBLIC_COLUMNS} FROM integrations WHERE provider = $1 AND external_workspace_id = $2`, [provider, externalWorkspaceId] ); return rows[0] ? toPublicRow(rows[0]) : null; }; export const upsertInstall = async (input: { provider: ProviderSlug; externalWorkspaceId: string; displayName?: string; apiKeyHash?: string; signingSecret?: string; scopes?: string[]; }): Promise => { const { rows } = await query( `INSERT INTO integrations (provider, external_workspace_id, display_name, api_key_hash, signing_secret_ciphertext, scopes) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (provider, external_workspace_id) DO UPDATE SET display_name = EXCLUDED.display_name, api_key_hash = COALESCE(EXCLUDED.api_key_hash, integrations.api_key_hash), signing_secret_ciphertext = COALESCE( EXCLUDED.signing_secret_ciphertext, integrations.signing_secret_ciphertext ), scopes = EXCLUDED.scopes, updated_at = NOW() RETURNING ${PUBLIC_COLUMNS}`, [ input.provider, input.externalWorkspaceId, input.displayName ?? null, input.apiKeyHash ?? null, input.signingSecret ? encryptSecret(input.signingSecret) : null, input.scopes ?? [], ] ); return toPublicRow(rows[0]); }; export const updateTokens = async (input: { id: number; accessToken: string; refreshToken?: string; expiresAt?: Date; }): Promise => { await query( `UPDATE integrations SET access_token_ciphertext = $2, refresh_token_ciphertext = COALESCE($3, refresh_token_ciphertext), token_expires_at = $4, updated_at = NOW() WHERE id = $1`, [ input.id, encryptSecret(input.accessToken), input.refreshToken ? encryptSecret(input.refreshToken) : null, input.expiresAt ?? null, ] ); }; const readSecret = async ( id: number, column: 'access_token_ciphertext' | 'refresh_token_ciphertext' | 'signing_secret_ciphertext' ): Promise => { const { rows } = await query>( `SELECT ${column} AS value FROM integrations WHERE id = $1`, [id] ); const value = rows[0]?.value; return value ? decryptSecret(value) : null; }; export const getAccessToken = (id: number): Promise => readSecret(id, 'access_token_ciphertext'); export const getRefreshToken = (id: number): Promise => readSecret(id, 'refresh_token_ciphertext'); export const getSigningSecret = (id: number): Promise => readSecret(id, 'signing_secret_ciphertext');