diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index d36c9c297..26d606f87 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -15,6 +15,7 @@ import { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, } from "../variables"; /** @@ -313,7 +314,40 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { }; const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { - return []; + // in place of accessToken we have to send Circle-Token i.e. Personal API token from CircleCi + let apps: any; + try { + const circleciOrganizationDetail = ( + await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { + headers: { + "Circle-Token": accessToken, + }, + }) + ).data; + + const { slug } = circleciOrganizationDetail; + + const res = ( + await axios.get( + `${INTEGRATION_CIRCLECI_API_URL}/v2/pipeline/?org-slug=${slug}`, + { + headers: { + "Circle-Token": accessToken, + }, + } + ) + ).data.items; + + apps = res.map((a: any) => ({ + name: a?.project_slug?.split("/")[2], + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to get Render services"); + } + + return apps; }; export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 9954943b8..ddc3329ec 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,10 +1,10 @@ -import axios from 'axios'; -import * as Sentry from '@sentry/node'; -import { Octokit } from '@octokit/rest'; +import axios from "axios"; +import * as Sentry from "@sentry/node"; +import { Octokit } from "@octokit/rest"; // import * as sodium from 'libsodium-wrappers'; -import sodium from 'libsodium-wrappers'; +import sodium from "libsodium-wrappers"; // const sodium = require('libsodium-wrappers'); -import { IIntegration, IIntegrationAuth } from '../models'; +import { IIntegration, IIntegrationAuth } from "../models"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, @@ -12,13 +12,15 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL -} from '../variables'; -import { access, appendFile } from 'fs'; + INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, +} from "../variables"; +import { access, appendFile } from "fs"; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -32,7 +34,7 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }: { integration: IIntegration; integrationAuth: IIntegrationAuth; @@ -45,7 +47,7 @@ const syncSecrets = async ({ await syncSecretsHeroku({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_VERCEL: @@ -53,7 +55,7 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }); break; case INTEGRATION_NETLIFY: @@ -61,35 +63,41 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }); break; case INTEGRATION_GITHUB: await syncSecretsGitHub({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_RENDER: await syncSecretsRender({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: await syncSecretsFlyio({ integration, secrets, - accessToken + accessToken, }); break; + // case INTEGRATION_CIRCLECI: + // await syncSecretsCircleci({ + // integration, + // secrets, + // accessToken, + // }); } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to integration'); + throw new Error("Failed to sync secrets to integration"); } }; @@ -103,7 +111,7 @@ const syncSecrets = async ({ const syncSecretsHeroku = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -115,9 +123,9 @@ const syncSecretsHeroku = async ({ `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, } ) ).data; @@ -133,15 +141,15 @@ const syncSecretsHeroku = async ({ secrets, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + throw new Error("Failed to sync secrets to Heroku"); } }; @@ -152,156 +160,168 @@ const syncSecretsHeroku = async ({ * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ const syncSecretsVercel = async ({ - integration, - integrationAuth, - secrets, - accessToken + integration, + integrationAuth, + secrets, + accessToken, }: { - integration: IIntegration, - integrationAuth: IIntegrationAuth, - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - interface VercelSecret { - id?: string; - type: string; - key: string; - value: string; - target: string[]; - } - - try { - // Get all (decrypted) secrets back from Vercel in - // decrypted format - const params: { [key: string]: string } = { - decrypt: 'true', - ...( integrationAuth?.teamId ? { - teamId: integrationAuth.teamId - } : {}) - } - - const res = (await Promise.all((await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - { + interface VercelSecret { + id?: string; + type: string; + key: string; + value: string; + target: string[]; + } + + try { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params: { [key: string]: string } = { + decrypt: "true", + ...(integrationAuth?.teamId + ? { + teamId: integrationAuth.teamId, + } + : {}), + }; + + const res = ( + await Promise.all( + ( + await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + { params, headers: { - Authorization: `Bearer ${accessToken}` - } - } - )) - .data - .envs - .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)) - .map(async (secret: VercelSecret) => (await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )).data) - )).reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const updateSecrets: VercelSecret[] = []; - const deleteSecrets: VercelSecret[] = []; - const newSecrets: VercelSecret[] = []; - - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.targetEnvironment] - }); - } - }); - - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.targetEnvironment] - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key: key, - value: res[key].value, - type: 'encrypted', - target: [integration.targetEnvironment], - }); - } - }); - - // Sync/push new secrets - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, - newSecrets, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - } - - // Sync/push updated secrets - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: VercelSecret) => { - const { - id, - ...updatedSecret - } = secret; - await axios.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - updatedSecret, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - // Delete secrets - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (secret: VercelSecret) => { - await axios.delete( + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).data.envs + .filter((secret: VercelSecret) => + secret.target.includes(integration.targetEnvironment) + ) + .map( + async (secret: VercelSecret) => + ( + await axios.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, { params, headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, } - ); - }); + ) + ).data + ) + ) + ).reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret, + }), + {} + ); + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; + + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: "encrypted", + target: [integration.targetEnvironment], + }); } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Vercel'); + }); + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: "encrypted", + target: [integration.targetEnvironment], + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: "encrypted", + target: [integration.targetEnvironment], + }); + } + }); + + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); } -} + + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { id, ...updatedSecret } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Vercel"); + } +}; /** * Sync/push [secrets] to Netlify site with id [integration.appId] @@ -312,202 +332,214 @@ const syncSecretsVercel = async ({ * @param {Object} obj.accessToken - access token for Netlify integration */ const syncSecretsNetlify = async ({ - integration, - integrationAuth, - secrets, - accessToken + integration, + integrationAuth, + secrets, + accessToken, }: { - integration: IIntegration; - integrationAuth: IIntegrationAuth; - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - try { - - interface NetlifyValue { - id?: string; - context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', - value: string; - } - - interface NetlifySecret { - key: string; - values: NetlifyValue[]; - } - - interface NetlifySecretsRes { - [index: string]: NetlifySecret; - } - - const getParams = new URLSearchParams({ - context_name: 'all', // integration.context or all - site_id: integration.appId - }); - - const res = (await axios.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - { - params: getParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )) - .data - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const newSecrets: NetlifySecret[] = []; // createEnvVars - const deleteSecrets: string[] = []; // deleteEnvVar - const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue - const updateSecrets: NetlifySecret[] = []; // setEnvVarValue - - // identify secrets to create and update - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: Infisical secret does not exist in Netlify -> create secret - newSecrets.push({ - key, - values: [{ - value: secrets[key], - context: integration.targetEnvironment - }] - }); - } else { - // case: Infisical secret exists in Netlify - const contexts = res[key].values - .reduce((obj: any, value: NetlifyValue) => ({ - ...obj, - [value.context]: value - }), {}); - - if (integration.targetEnvironment in contexts) { - // case: Netlify secret value exists in integration context - if (secrets[key] !== contexts[integration.targetEnvironment].value) { - // case: Infisical and Netlify secret values are different - // -> update Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.targetEnvironment, - value: secrets[key] - }] - }); - } - } else { - // case: Netlify secret value does not exist in integration context - // -> add the new Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.targetEnvironment, - value: secrets[key] - }] - }); - } - } - }) - - // identify secrets to delete - // TODO: revise (patch case where 1 context was deleted but others still there - Object.keys(res).map((key) => { - // loop through each key's context - if (!(key in secrets)) { - // case: Netlify secret does not exist in Infisical - - const numberOfValues = res[key].values.length; - - res[key].values.forEach((value: NetlifyValue) => { - if (value.context === integration.targetEnvironment) { - if (numberOfValues <= 1) { - // case: Netlify secret value has less than 1 context -> delete secret - deleteSecrets.push(key); - } else { - // case: Netlify secret value has more than 1 context -> delete secret value context - deleteSecretValues.push({ - key, - values: [{ - id: value.id, - context: integration.targetEnvironment, - value: value.value - }] - }); - } - } - }); - } - }); - - const syncParams = new URLSearchParams({ - site_id: integration.appId - }); - - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - newSecrets, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - } - - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: NetlifySecret) => { - await axios.patch( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - context: secret.values[0].context, - value: secret.values[0].value - }, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (key: string) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - if (deleteSecretValues.length > 0) { - deleteSecretValues.forEach(async (secret: NetlifySecret) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + try { + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; } -} + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } + + const getParams = new URLSearchParams({ + context_name: "all", // integration.context or all + site_id: integration.appId, + }); + + const res = ( + await axios.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret, + }), + {} + ); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [ + { + value: secrets[key], + context: integration.targetEnvironment, + }, + ], + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values.reduce( + (obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value, + }), + {} + ); + + if (integration.targetEnvironment in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.targetEnvironment].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment, + value: secrets[key], + }, + ], + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment, + value: secrets[key], + }, + ], + }); + } + } + }); + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.targetEnvironment) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [ + { + id: value.id, + context: integration.targetEnvironment, + value: value.value, + }, + ], + }); + } + } + }); + } + }); + + const syncParams = new URLSearchParams({ + site_id: integration.appId, + }); + + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + } + + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await axios.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value, + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Heroku"); + } +}; /** * Sync/push [secrets] to GitHub repo with name [integration.app] @@ -515,24 +547,23 @@ const syncSecretsNetlify = async ({ * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) - * @param {String} obj.accessToken - access token for GitHub integration + * @param {String} obj.accessToken - access token for GitHub integration */ const syncSecretsGitHub = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { - interface GitHubRepoKey { key_id: string; key: string; } - + interface GitHubSecret { name: string; created_at: string; @@ -540,87 +571,88 @@ const syncSecretsGitHub = async ({ } interface GitHubSecretRes { - [index: string]: GitHubSecret; + [index: string]: GitHubSecret; } const deleteSecrets: GitHubSecret[] = []; const octokit = new Octokit({ - auth: accessToken + auth: accessToken, }); // const user = (await octokit.request('GET /user', {})).data; - const repoPublicKey: GitHubRepoKey = (await octokit.request( - 'GET /repos/{owner}/{repo}/actions/secrets/public-key', - { - owner: integration.owner, - repo: integration.app - } - )).data; + const repoPublicKey: GitHubRepoKey = ( + await octokit.request( + "GET /repos/{owner}/{repo}/actions/secrets/public-key", + { + owner: integration.owner, + repo: integration.app, + } + ) + ).data; // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key - const encryptedSecrets: GitHubSecretRes = (await octokit.request( - 'GET /repos/{owner}/{repo}/actions/secrets', - { + const encryptedSecrets: GitHubSecretRes = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { owner: integration.owner, - repo: integration.app - } - )) - .data - .secrets - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.name]: secret - }), {}); - + repo: integration.app, + }) + ).data.secrets.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret, + }), + {} + ); + Object.keys(encryptedSecrets).map(async (key) => { if (!(key in secrets)) { await octokit.request( - 'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', + "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { owner: integration.owner, repo: integration.app, - secret_name: key + secret_name: key, } ); } }); - + Object.keys(secrets).map((key) => { // let encryptedSecret; sodium.ready.then(async () => { - // convert secret & base64 key to Uint8Array. - const binkey = sodium.from_base64( - repoPublicKey.key, - sodium.base64_variants.ORIGINAL - ); - const binsec = sodium.from_string(secrets[key]); + // convert secret & base64 key to Uint8Array. + const binkey = sodium.from_base64( + repoPublicKey.key, + sodium.base64_variants.ORIGINAL + ); + const binsec = sodium.from_string(secrets[key]); - // encrypt secret using libsodium - const encBytes = sodium.crypto_box_seal(binsec, binkey); + // encrypt secret using libsodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); - // convert encrypted Uint8Array to base64 - const encryptedSecret = sodium.to_base64( - encBytes, - sodium.base64_variants.ORIGINAL - ); - - await octokit.request( - 'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', - { - owner: integration.owner, - repo: integration.app, - secret_name: key, - encrypted_value: encryptedSecret, - key_id: repoPublicKey.key_id - } - ); + // convert encrypted Uint8Array to base64 + const encryptedSecret = sodium.to_base64( + encBytes, + sodium.base64_variants.ORIGINAL + ); + + await octokit.request( + "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", + { + owner: integration.owner, + repo: integration.app, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id, + } + ); }); }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to GitHub'); + throw new Error("Failed to sync secrets to GitHub"); } }; @@ -634,7 +666,7 @@ const syncSecretsGitHub = async ({ const syncSecretsRender = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -645,20 +677,20 @@ const syncSecretsRender = async ({ `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, Object.keys(secrets).map((key) => ({ key, - value: secrets[key] + value: secrets[key], })), { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Render'); + throw new Error("Failed to sync secrets to Render"); } -} +}; /** * Sync/push [secrets] to Fly.io app @@ -670,7 +702,7 @@ const syncSecretsRender = async ({ const syncSecretsFlyio = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -700,28 +732,31 @@ const syncSecretsFlyio = async ({ await axios({ url: INTEGRATION_FLYIO_API_URL, - method: 'post', + method: "post", headers: { - 'Authorization': 'Bearer ' + accessToken + Authorization: "Bearer " + accessToken, }, data: { query: SetSecrets, variables: { input: { appId: integration.app, - secrets: Object.entries(secrets).map(([key, value]) => ({ key, value })) - } - } - } + secrets: Object.entries(secrets).map(([key, value]) => ({ + key, + value, + })), + }, + }, + }, }); - + // get secrets interface FlyioSecret { name: string; digest: string; createdAt: string; } - + const GetSecrets = `query ($appName: String!) { app(name: $appName) { secrets { @@ -732,25 +767,27 @@ const syncSecretsFlyio = async ({ } }`; - const getSecretsRes = (await axios({ - method: 'post', + const getSecretsRes = ( + await axios({ + method: "post", url: INTEGRATION_FLYIO_API_URL, headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", }, data: { query: GetSecrets, variables: { - appName: integration.app - } - } - })).data.data.app.secrets; - + appName: integration.app, + }, + }, + }) + ).data.data.app.secrets; + const deleteSecretsKeys = getSecretsRes .filter((secret: FlyioSecret) => !(secret.name in secrets)) .map((secret: FlyioSecret) => secret.name); - + // unset (delete) secrets const DeleteSecrets = `mutation($input: UnsetSecretsInput!) { unsetSecrets(input: $input) { @@ -771,28 +808,37 @@ const syncSecretsFlyio = async ({ }`; await axios({ - method: 'post', - url: INTEGRATION_FLYIO_API_URL, - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' + method: "post", + url: INTEGRATION_FLYIO_API_URL, + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + data: { + query: DeleteSecrets, + variables: { + input: { + appId: integration.app, + keys: deleteSecretsKeys, + }, }, - data: { - query: DeleteSecrets, - variables: { - input: { - appId: integration.app, - keys: deleteSecretsKeys - } - } - } + }, }); - } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Fly.io'); + throw new Error("Failed to sync secrets to Fly.io"); } -} +}; -export { syncSecrets }; \ No newline at end of file +// const syncSecretsCircleci = async ({ +// integration, +// secrets, +// accessToken, +// }: { +// integration: IIntegration; +// secrets: any; +// accessToken: string; +// }) => {}; + +export { syncSecrets }; diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index ed4be5d16..4196a374f 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -24,6 +24,7 @@ import { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, INTEGRATION_OPTIONS, } from "./integration"; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; @@ -69,6 +70,7 @@ export { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, ACTION_ADD_SECRETS, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index bcb161f81..0ae70303b 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -40,7 +40,7 @@ const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; const INTEGRATION_RENDER_API_URL = "https://api.render.com"; const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; -const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api/v2"; +const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_OPTIONS = [ { @@ -164,5 +164,6 @@ export { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, INTEGRATION_OPTIONS, };