Merge branch 'main' of github.com:reginaldbondoc/infisical

This commit is contained in:
Reginald Bondoc
2022-12-20 01:04:33 +01:00
97 changed files with 4294 additions and 1302 deletions
+169
View File
@@ -0,0 +1,169 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
} from '../variables';
/**
* Return list of names of apps for integration named [integration]
* @param {Object} obj
* @param {String} obj.integration - name of integration
* @param {String} obj.accessToken - access token for integration
* @returns {Object[]} apps - names of integration apps
* @returns {String} apps.name - name of integration app
*/
const getApps = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
interface App {
name: string;
siteId?: string;
}
let apps: App[]; // TODO: add type and define payloads for apps
try {
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
accessToken
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
integrationAuth,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration apps');
}
return apps;
}
/**
* Return list of names of apps for Heroku integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Heroku API
* @returns {Object[]} apps - names of Heroku apps
* @returns {String} apps.name - name of Heroku app
*/
const getAppsHeroku = async ({
accessToken
}: {
accessToken: string;
}) => {
let apps;
try {
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
})).data;
apps = res.map((a: any) => ({
name: a.name
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Heroku integration apps');
}
return apps;
}
/**
* Return list of names of apps for Vercel integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Vercel API
* @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app
*/
const getAppsVercel = async ({
accessToken
}: {
accessToken: string;
}) => {
let apps;
try {
const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})).data;
apps = res.projects.map((a: any) => ({
name: a.name
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps');
}
return apps;
}
/**
* Return list of names of sites for Netlify integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Netlify API
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})).data;
apps = res.map((a: any) => ({
name: a.name,
siteId: a.site_id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
}
return apps;
}
export {
getApps
}
+241
View File
@@ -0,0 +1,241 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
ACTION_PUSH_TO_HEROKU
} from '../variables';
import {
SITE_URL,
CLIENT_SECRET_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY
} from '../config';
interface ExchangeCodeHerokuResponse {
token_type: string;
access_token: string;
expires_in: number;
refresh_token: string;
user_id: string;
session_nonce?: string;
}
interface ExchangeCodeVercelResponse {
token_type: string;
access_token: string;
installation_id: string;
user_id: string;
team_id?: string;
}
interface ExchangeCodeNetlifyResponse {
access_token: string;
token_type: string;
refresh_token: string;
scope: string;
created_at: number;
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration]
* @param {Object} obj1
* @param {String} obj1.integration - name of integration
* @param {String} obj1.code - code for code-token exchange
* @returns {Object} obj
* @returns {String} obj.accessToken - access token for integration
* @returns {String} obj.refreshToken - refresh token for integration
* @returns {Date} obj.accessExpiresAt - date of expiration for access token
* @returns {String} obj.action - integration action for bot sequence
*/
const exchangeCode = async ({
integration,
code
}: {
integration: string;
code: string;
}) => {
let obj = {} as any;
try {
switch (integration) {
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code
});
break;
case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({
code
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange');
}
return obj;
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
* OAuth2 code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for Heroku API
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeHeroku = async ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeHerokuResponse;
const accessExpiresAt = new Date();
try {
res = (await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_secret: CLIENT_SECRET_HEROKU
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Heroku');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for Heroku API
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeVercel = async ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeVercelResponse;
try {
res = (await axios.post(
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel`
} as any)
)).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Vercel');
}
return ({
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null,
teamId: res.team_id
});
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for Heroku API
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeNetlify = async ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeNetlifyResponse;
let accountId;
try {
res = (await axios.post(
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify`
} as any)
)).data;
const res2 = await axios.get(
'https://api.netlify.com/api/v1/sites',
{
headers: {
Authorization: `Bearer ${res.access_token}`
}
}
);
const res3 = (await axios.get(
'https://api.netlify.com/api/v1/accounts',
{
headers: {
Authorization: `Bearer ${res.access_token}`
}
}
)).data;
accountId = res3[0].id;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId
});
}
export {
exchangeCode
}
+13
View File
@@ -0,0 +1,13 @@
import { exchangeCode } from './exchange';
import { exchangeRefresh } from './refresh';
import { getApps } from './apps';
import { syncSecrets } from './sync';
import { revokeAccess } from './revoke';
export {
exchangeCode,
exchangeRefresh,
getApps,
syncSecrets,
revokeAccess
}
+78
View File
@@ -0,0 +1,78 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { INTEGRATION_HEROKU } from '../variables';
import {
CLIENT_SECRET_HEROKU
} from '../config';
import {
INTEGRATION_HEROKU_TOKEN_URL
} from '../variables';
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
* @param {Object} obj
* @param {String} obj.integration - name of integration
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
*/
const exchangeRefresh = async ({
integration,
refreshToken
}: {
integration: string;
refreshToken: string;
}) => {
let accessToken;
try {
switch (integration) {
case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({
refreshToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
}
return accessToken;
}
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
* @returns
*/
const exchangeRefreshHeroku = async ({
refreshToken
}: {
refreshToken: string;
}) => {
let accessToken;
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token for Heroku');
}
return accessToken;
}
export {
exchangeRefresh
}
+50
View File
@@ -0,0 +1,50 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
IIntegrationAuth,
IntegrationAuth,
Integration
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
const revokeAccess = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth,
accessToken: string
}) => {
try {
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
break;
case INTEGRATION_VERCEL:
break;
case INTEGRATION_NETLIFY:
break;
}
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
});
if (deletedIntegrationAuth) {
await Integration.deleteMany({
integrationAuth: deletedIntegrationAuth._id
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete integration authorization');
}
}
export {
revokeAccess
}
+598
View File
@@ -0,0 +1,598 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
IIntegration, IIntegrationAuth
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
} from '../variables';
// TODO: need a helper function in the future to handle integration
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
/**
* Sync/push [secrets] to [app] in integration named [integration]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.app - app in integration
* @param {Object} obj.target - (optional) target (environment) in integration
* @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 integration
*/
const syncSecrets = async ({
integration,
integrationAuth,
secrets,
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
try {
switch (integration.integration) {
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_VERCEL:
await syncSecretsVercel({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_NETLIFY:
await syncSecretsNetlify({
integration,
integrationAuth,
secrets,
accessToken
});
break;
}
// TODO: set integration to inactive if it was not synced correctly (send alert?)
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integration');
}
}
/**
* Sync/push [secrets] to Heroku [app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/
const syncSecretsHeroku = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration,
secrets: any;
accessToken: string;
}) => {
try {
const herokuSecrets = (await axios.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
}
)).data;
Object.keys(herokuSecrets).forEach(key => {
if (!(key in secrets)) {
secrets[key] = null;
}
});
await axios.patch(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
secrets,
{
headers: {
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');
}
}
/**
* Sync/push [secrets] to Heroku [app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/
const syncSecretsVercel = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration,
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 = new URLSearchParams({
decrypt: "true"
});
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.target))
.map(async (secret: VercelSecret) => (await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
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.target]
});
}
});
// 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.target]
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
id: res[key].id,
key: key,
value: res[key].value,
type: 'encrypted',
target: [integration.target],
});
}
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets,
{
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,
{
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}`,
{
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 [app]
* @param {Object} obj
* @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)
*/
const syncSecretsNetlify = async ({
integration,
integrationAuth,
secrets,
accessToken
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
// TODO: Netlify revision in progress
// try {
// const getParams = new URLSearchParams({
// context_name: integration.context,
// site_id: integration.siteId
// });
// 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
// }), {});
// res.forEach((r: any) => console.log(r));
// console.log('getParams', getParams);
// interface UpdateNetlifySecret {
// key: string;
// context: string;
// value: string;
// }
// interface DeleteNetlifySecret {
// key: string;
// }
// interface NewNetlifySecretValue {
// value: string;
// context: string;
// }
// interface NewNetlifySecret {
// key: string;
// values: NewNetlifySecretValue[];
// }
// let updateSecrets: UpdateNetlifySecret[] = [];
// let deleteSecrets: DeleteNetlifySecret[] = [];
// let newSecrets: NewNetlifySecret[] = [];
// interface NetlifyValue {
// id: string;
// value: string;
// context: string;
// role: string;
// }
// // Identify secrets to create - GOOD
// Object.keys(secrets).map((key) => {
// if (!(key in res)) {
// // case: secret does not exist in Netlify -> create secret
// newSecrets.push({
// key: key,
// values: [{
// value: secrets[key],
// context: integration.context
// }]
// });
// } else {
// // case: secret exists in Netlify
// const netlifyContextsSet = new Set (res[key].values.map((netlifyValue: NetlifyValue) => netlifyValue.context));
// // TODO: check context/env.
// res[key].values.forEach((netlifyValue: NetlifyValue) => {
// if (netlifyValue.context === integration.context) {
// // case: Netlify value context matches integration context
// // TODO: check if value has changed
// }
// });
// }
// });
// // Identify secrets to update and delete
// Object.keys(res).map((key) => {
// if (key in secrets) {
// // if (res[key] !== secrets[key]) {
// // // case: secret value has changed
// // updateSecrets.push({
// // key: key,
// // context: integration.context,
// // value: secrets[key]
// // });
// // }
// // TODO: modify check and record of updated secrets
// // case 1: new context added.
// } else {
// // case: secret has been deleted
// deleteSecrets.push({
// key
// });
// }
// });
// const syncParams = new URLSearchParams({
// site_id: integration.siteId
// });
// console.log('Netlify newSecrets', newSecrets);
// newSecrets.forEach(secret => console.log(secret.values));
// // Sync/push new secrets
// if (newSecrets.length > 0) {
// await axios.post(
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
// newSecrets,
// {
// params: syncParams,
// headers: {
// Authorization: `Bearer ${accessToken}`
// }
// }
// );
// }
// console.log('Netlify updateSecrets', updateSecrets);
// // Sync/push updated secrets
// if (updateSecrets.length > 0) {
// updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
// await axios.patch(
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
// {
// context: secret.context,
// value: secret.value
// },
// {
// params: syncParams,
// headers: {
// Authorization: `Bearer ${accessToken}`
// }
// }
// );
// });
// }
// console.log('Netlify deleteSecrets', deleteSecrets);
// // Delete secrets
// if (deleteSecrets.length > 0) {
// deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
// await axios.delete(
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
// {
// params: syncParams,
// headers: {
// Authorization: `Bearer ${accessToken}`
// }
// }
// );
// });
// }
// } catch (err) {
// Sentry.setUser(null);
// Sentry.captureException(err);
// throw new Error('Failed to sync secrets to Heroku');
// }
try {
const getParams = new URLSearchParams({
context_name: integration.context,
site_id: integration.siteId
});
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.values[0].value
}), {});
interface UpdateNetlifySecret {
key: string;
context: string;
value: string;
}
interface DeleteNetlifySecret {
key: string;
}
interface NewNetlifySecretValue {
value: string;
context: string;
}
interface NewNetlifySecret {
key: string;
values: NewNetlifySecretValue[];
}
const updateSecrets: UpdateNetlifySecret[] = [];
const deleteSecrets: DeleteNetlifySecret[] = [];
const newSecrets: NewNetlifySecret[] = [];
// Identify secrets to create
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: secret has been created
newSecrets.push({
key: key,
values: [{
value: secrets[key], // include id?
context: integration.context
}]
});
}
});
// Identify secrets to update and delete
Object.keys(res).map((key) => {
if (key in secrets) {
if (res[key] !== secrets[key]) {
// case: secret value has changed
updateSecrets.push({
key: key,
context: integration.context,
value: secrets[key]
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
key
});
}
});
const syncParams = new URLSearchParams({
site_id: integration.siteId
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
newSecrets,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
// Sync/push updated secrets
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
await axios.patch(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.context,
value: secret.value
},
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
// Delete secrets
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
}
}
export {
syncSecrets
}