import sodium from "libsodium-wrappers" import { type Octokit } from "octokit" import * as Sentry from "@sentry/node" /** * Encrypts a secret value using GitHub's public key encryption (NaCl box seal) * @param publicKey - Base64-encoded public key from GitHub * @param secretValue - The plaintext secret value to encrypt * @returns Object containing encrypted_value (base64) and key_id */ export async function encryptSecret( publicKey: string, secretValue: string, ): Promise<{ encrypted_value: string; key_id: string }> { await sodium.ready console.log( `[secret-utils.ts:encryptSecret] Encrypting secret value (length: ${secretValue.length})`, ) // Decode the public key from base64 const publicKeyBytes = Buffer.from(publicKey, "base64") // Convert secret to bytes const secretBytes = sodium.from_string(secretValue) // Encrypt using crypto_box_seal (NaCl box seal encryption) // This is the format GitHub expects for repository secrets const encryptedBytes = sodium.crypto_box_seal(secretBytes, publicKeyBytes) // Convert encrypted bytes to base64 const encryptedValue = Buffer.from(encryptedBytes).toString("base64") console.log( `[secret-utils.ts:encryptSecret] Successfully encrypted secret (encrypted length: ${encryptedValue.length})`, ) // Note: key_id is returned from GitHub's public key endpoint, not generated here // We'll extract it from the getRepositoryPublicKey response return { encrypted_value: encryptedValue, key_id: "", // Will be set by caller from public key response } } /** * Gets the repository's public key for encrypting secrets * @param octokit - Authenticated Octokit instance * @param owner - Repository owner * @param repo - Repository name * @returns Object containing public_key (base64) and key_id */ export async function getRepositoryPublicKey( octokit: Octokit, owner: string, repo: string, ): Promise<{ public_key: string; key_id: string }> { try { console.log(`[secret-utils.ts:getRepositoryPublicKey] Getting public key for ${owner}/${repo}`) const response = await octokit.rest.actions.getRepoPublicKey({ owner, repo, }) console.log( `[secret-utils.ts:getRepositoryPublicKey] Successfully retrieved public key for ${owner}/${repo}, key_id=${response.data.key_id}`, ) return { public_key: response.data.key, key_id: response.data.key_id, } } catch (error: any) { console.error( `[secret-utils.ts:getRepositoryPublicKey] Error getting public key for ${owner}/${repo}:`, error, ) Sentry.captureException(error) throw error } } /** * Creates or updates a repository secret * @param octokit - Authenticated Octokit instance * @param owner - Repository owner * @param repo - Repository name * @param secretName - Name of the secret (e.g., "CODEFLASH_API_KEY") * @param encryptedValue - Base64-encoded encrypted secret value * @param keyId - The key ID from the repository's public key */ export async function createOrUpdateSecret( octokit: Octokit, owner: string, repo: string, secretName: string, encryptedValue: string, keyId: string, ): Promise { try { console.log( `[secret-utils.ts:createOrUpdateSecret] Creating/updating secret ${secretName} for ${owner}/${repo}`, ) await octokit.rest.actions.createOrUpdateRepoSecret({ owner, repo, secret_name: secretName, encrypted_value: encryptedValue, key_id: keyId, }) console.log( `[secret-utils.ts:createOrUpdateSecret] Successfully created/updated secret ${secretName} for ${owner}/${repo}`, ) } catch (error: any) { console.error( `[secret-utils.ts:createOrUpdateSecret] Error creating/updating secret ${secretName} for ${owner}/${repo}:`, error, ) Sentry.captureException(error) // Check if it's a permission error (403) if (error.status === 403) { const errorMsg = "The GitHub App does not have 'secrets: write' permission, or organization policies prevent secret creation." throw new Error(errorMsg) } throw error } } /** * Complete helper function to encrypt and store a secret in one call * @param octokit - Authenticated Octokit instance * @param owner - Repository owner * @param repo - Repository name * @param secretName - Name of the secret (e.g., "CODEFLASH_API_KEY") * @param secretValue - Plaintext secret value to encrypt and store */ export async function encryptAndStoreSecret( octokit: Octokit, owner: string, repo: string, secretName: string, secretValue: string, ): Promise { console.log( `[secret-utils.ts:encryptAndStoreSecret] Starting encryption and storage of secret ${secretName} for ${owner}/${repo}`, ) // Step 1: Get repository public key const { public_key, key_id } = await getRepositoryPublicKey(octokit, owner, repo) // Step 2: Encrypt the secret const { encrypted_value } = await encryptSecret(public_key, secretValue) // Step 3: Store the encrypted secret await createOrUpdateSecret(octokit, owner, repo, secretName, encrypted_value, key_id) console.log( `[secret-utils.ts:encryptAndStoreSecret] Successfully encrypted and stored secret ${secretName} for ${owner}/${repo}`, ) }