Merge branch 'main' into ENG-3259

This commit is contained in:
x032205
2025-07-17 15:41:19 -04:00
185 changed files with 3501 additions and 178 deletions
@@ -410,7 +410,7 @@ export const samlConfigServiceFactory = ({
}
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted);
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = crypto.jwt().sign(
{
+14
View File
@@ -2283,6 +2283,13 @@ export const AppConnections = {
},
RAILWAY: {
apiToken: "The API token used to authenticate with Railway."
},
CHECKLY: {
apiKey: "The API key used to authenticate with Checkly."
},
SUPABASE: {
accessKey: "The Key used to access Supabase.",
instanceUrl: "The URL used to access Supabase."
}
}
};
@@ -2489,6 +2496,13 @@ export const SecretSyncs = {
environmentName: "The Railway environment to sync secrets to.",
serviceId: "The Railway service that secrets should be synced to.",
serviceName: "The Railway service that secrets should be synced to."
},
CHECKLY: {
accountId: "The ID of the Checkly account to sync secrets to."
},
SUPABASE: {
projectId: "The ID of the Supabase project to sync secrets to.",
projectName: "The name of the Supabase project to sync secrets to."
}
}
};
@@ -39,6 +39,10 @@ import {
CamundaConnectionListItemSchema,
SanitizedCamundaConnectionSchema
} from "@app/services/app-connection/camunda";
import {
ChecklyConnectionListItemSchema,
SanitizedChecklyConnectionSchema
} from "@app/services/app-connection/checkly";
import {
CloudflareConnectionListItemSchema,
SanitizedCloudflareConnectionSchema
@@ -79,6 +83,10 @@ import {
RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema
} from "@app/services/app-connection/render/render-connection-schema";
import {
SanitizedSupabaseConnectionSchema,
SupabaseConnectionListItemSchema
} from "@app/services/app-connection/supabase";
import {
SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema
@@ -128,7 +136,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedCloudflareConnectionSchema.options,
...SanitizedBitbucketConnectionSchema.options,
...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options
...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -163,7 +173,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
CloudflareConnectionListItemSchema,
BitbucketConnectionListItemSchema,
ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema
RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,56 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateChecklyConnectionSchema,
SanitizedChecklyConnectionSchema,
UpdateChecklyConnectionSchema
} from "@app/services/app-connection/checkly";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerChecklyConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Checkly,
server,
sanitizedResponseSchema: SanitizedChecklyConnectionSchema,
createSchema: CreateChecklyConnectionSchema,
updateSchema: UpdateChecklyConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/accounts`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
accounts: z
.object({
name: z.string(),
id: z.string(),
runtimeId: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const accounts = await server.services.appConnection.checkly.listAccounts(connectionId, req.permission);
return { accounts };
}
});
};
@@ -11,6 +11,7 @@ import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-r
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
@@ -27,6 +28,7 @@ import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router";
@@ -68,5 +70,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter
[AppConnection.Railway]: registerRailwayConnectionRouter,
[AppConnection.Checkly]: registerChecklyConnectionRouter,
[AppConnection.Supabase]: registerSupabaseConnectionRouter
};
@@ -0,0 +1,55 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateSupabaseConnectionSchema,
SanitizedSupabaseConnectionSchema,
UpdateSupabaseConnectionSchema
} from "@app/services/app-connection/supabase";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerSupabaseConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Supabase,
server,
sanitizedResponseSchema: SanitizedSupabaseConnectionSchema,
createSchema: CreateSupabaseConnectionSchema,
updateSchema: UpdateSupabaseConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
projects: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.supabase.listProjects(connectionId, req.permission);
return { projects };
}
});
};
@@ -28,7 +28,17 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
.object({
authorization: z.string(),
host: z.string(),
"x-date": z.string()
"x-date": z.string().optional(),
date: z.string().optional()
})
.superRefine((val, ctx) => {
if (!val.date && !val["x-date"]) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Either date or x-date must be provided",
path: ["headers", "date"]
});
}
})
.describe(OCI_AUTH.LOGIN.headers)
}),
@@ -0,0 +1,17 @@
import {
ChecklySyncSchema,
CreateChecklySyncSchema,
UpdateChecklySyncSchema
} from "@app/services/secret-sync/checkly/checkly-sync-schemas";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerChecklySyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Checkly,
server,
responseSchema: ChecklySyncSchema,
createSchema: CreateChecklySyncSchema,
updateSchema: UpdateChecklySyncSchema
});
@@ -8,6 +8,7 @@ import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configurati
import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerCamundaSyncRouter } from "./camunda-sync-router";
import { registerChecklySyncRouter } from "./checkly-sync-router";
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
@@ -20,6 +21,7 @@ import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router";
import { registerSupabaseSyncRouter } from "./supabase-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router";
@@ -52,7 +54,8 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.GitLab]: registerGitLabSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter,
[SecretSync.CloudflareWorkers]: registerCloudflareWorkersSyncRouter,
[SecretSync.Supabase]: registerSupabaseSyncRouter,
[SecretSync.Zabbix]: registerZabbixSyncRouter,
[SecretSync.Railway]: registerRailwaySyncRouter
[SecretSync.Railway]: registerRailwaySyncRouter,
[SecretSync.Checkly]: registerChecklySyncRouter
};
@@ -22,6 +22,7 @@ import {
import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/services/secret-sync/azure-devops";
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
import {
CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema
@@ -40,6 +41,7 @@ import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -70,9 +72,10 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
GitLabSyncSchema,
CloudflarePagesSyncSchema,
CloudflareWorkersSyncSchema,
SupabaseSyncSchema,
ZabbixSyncSchema,
RailwaySyncSchema
RailwaySyncSchema,
ChecklySyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -101,7 +104,9 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
CloudflareWorkersSyncListItemSchema,
ZabbixSyncListItemSchema,
RailwaySyncListItemSchema
RailwaySyncListItemSchema,
ChecklySyncListItemSchema,
SupabaseSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,17 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
CreateSupabaseSyncSchema,
SupabaseSyncSchema,
UpdateSupabaseSyncSchema
} from "@app/services/secret-sync/supabase";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerSupabaseSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Supabase,
server,
responseSchema: SupabaseSyncSchema,
createSchema: CreateSupabaseSyncSchema,
updateSchema: UpdateSupabaseSyncSchema
});
@@ -31,12 +31,16 @@ export const validateOnePassConnectionCredentials = async (config: TOnePassConne
const { apiToken } = config.credentials;
try {
await request.get(`${instanceUrl}/v1/vaults`, {
const res = await request.get(`${instanceUrl}/v1/vaults`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
if (!Array.isArray(res.data)) {
throw new AxiosError("Invalid response from 1Password API");
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
@@ -30,7 +30,9 @@ export enum AppConnection {
Cloudflare = "cloudflare",
Zabbix = "zabbix",
Railway = "railway",
Bitbucket = "bitbucket"
Bitbucket = "bitbucket",
Checkly = "checkly",
Supabase = "supabase"
}
export enum AWSRegion {
@@ -57,6 +57,7 @@ import {
validateBitbucketConnectionCredentials
} from "./bitbucket";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
import {
getCloudflareConnectionListItem,
@@ -95,6 +96,11 @@ import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postg
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import {
getSupabaseConnectionListItem,
SupabaseConnectionMethod,
validateSupabaseConnectionCredentials
} from "./supabase";
import {
getTeamCityConnectionListItem,
TeamCityConnectionMethod,
@@ -147,7 +153,9 @@ export const listAppConnectionOptions = () => {
getCloudflareConnectionListItem(),
getZabbixConnectionListItem(),
getRailwayConnectionListItem(),
getBitbucketConnectionListItem()
getBitbucketConnectionListItem(),
getChecklyConnectionListItem(),
getSupabaseConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -231,7 +239,9 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
@@ -289,7 +299,10 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case LdapConnectionMethod.SimpleBind:
return "Simple Bind";
case RenderConnectionMethod.ApiKey:
case ChecklyConnectionMethod.ApiKey:
return "API Key";
case SupabaseConnectionMethod.AccessToken:
return "Access Token";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -352,7 +365,9 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
[AppConnection.Supabase]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
@@ -32,7 +32,9 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.Zabbix]: "Zabbix",
[AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket"
[AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Checkly]: "Checkly",
[AppConnection.Supabase]: "Supabase"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -67,5 +69,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
[AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
[AppConnection.Supabase]: AppConnectionPlanType.Regular
};
@@ -50,6 +50,8 @@ import { ValidateBitbucketConnectionCredentialsSchema } from "./bitbucket";
import { bitbucketConnectionService } from "./bitbucket/bitbucket-connection-service";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service";
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
import { checklyConnectionService } from "./checkly/checkly-connection-service";
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
@@ -77,6 +79,8 @@ import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service";
import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase";
import { supabaseConnectionService } from "./supabase/supabase-connection-service";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
@@ -130,7 +134,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -552,6 +558,8 @@ export const appConnectionServiceFactory = ({
cloudflare: cloudflareConnectionService(connectAppConnectionById),
zabbix: zabbixConnectionService(connectAppConnectionById),
railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById)
bitbucket: bitbucketConnectionService(connectAppConnectionById),
checkly: checklyConnectionService(connectAppConnectionById),
supabase: supabaseConnectionService(connectAppConnectionById)
};
};
@@ -69,6 +69,12 @@ import {
TCamundaConnectionInput,
TValidateCamundaConnectionCredentialsSchema
} from "./camunda";
import {
TChecklyConnection,
TChecklyConnectionConfig,
TChecklyConnectionInput,
TValidateChecklyConnectionCredentialsSchema
} from "./checkly";
import {
TCloudflareConnection,
TCloudflareConnectionConfig,
@@ -154,6 +160,12 @@ import {
TRenderConnectionInput,
TValidateRenderConnectionCredentialsSchema
} from "./render/render-connection-types";
import {
TSupabaseConnection,
TSupabaseConnectionConfig,
TSupabaseConnectionInput,
TValidateSupabaseConnectionCredentialsSchema
} from "./supabase";
import {
TTeamCityConnection,
TTeamCityConnectionConfig,
@@ -218,6 +230,8 @@ export type TAppConnection = { id: string } & (
| TBitbucketConnection
| TZabbixConnection
| TRailwayConnection
| TChecklyConnection
| TSupabaseConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -257,6 +271,8 @@ export type TAppConnectionInput = { id: string } & (
| TBitbucketConnectionInput
| TZabbixConnectionInput
| TRailwayConnectionInput
| TChecklyConnectionInput
| TSupabaseConnectionInput
);
export type TSqlConnectionInput =
@@ -303,7 +319,9 @@ export type TAppConnectionConfig =
| TCloudflareConnectionConfig
| TBitbucketConnectionConfig
| TZabbixConnectionConfig
| TRailwayConnectionConfig;
| TRailwayConnectionConfig
| TChecklyConnectionConfig
| TSupabaseConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -337,7 +355,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateCloudflareConnectionCredentialsSchema
| TValidateBitbucketConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema;
| TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema
| TValidateSupabaseConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -0,0 +1,3 @@
export enum ChecklyConnectionMethod {
ApiKey = "api-key"
}
@@ -0,0 +1,35 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
import { ChecklyPublicAPI } from "./checkly-connection-public-client";
import { TChecklyConnectionConfig } from "./checkly-connection-types";
export const getChecklyConnectionListItem = () => {
return {
name: "Checkly" as const,
app: AppConnection.Checkly as const,
methods: Object.values(ChecklyConnectionMethod)
};
};
export const validateChecklyConnectionCredentials = async (config: TChecklyConnectionConfig) => {
try {
await ChecklyPublicAPI.healthcheck(config);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection - verify credentials"
});
}
return config.credentials;
};
@@ -0,0 +1,186 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable class-methods-use-this */
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxiosError } from "axios";
import { createRequestClient } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types";
export function getChecklyAuthHeaders(
connection: TChecklyConnectionConfig,
accountId?: string
): Record<string, string> {
switch (connection.method) {
case ChecklyConnectionMethod.ApiKey:
return {
Authorization: `Bearer ${connection.credentials.apiKey}`,
...(accountId && { "X-Checkly-Account": accountId })
};
default:
throw new Error(`Unsupported Checkly connection method`);
}
}
export function getChecklyRatelimiter(response: AxiosResponse): {
maxAttempts: number;
isRatelimited: boolean;
wait: () => Promise<void>;
} {
const wait = () => {
return new Promise<void>((res) => {
setTimeout(res, 60 * 1000); // Wait for 60 seconds
});
};
return {
isRatelimited: response.status === HttpStatusCode.TooManyRequests,
wait,
maxAttempts: 3
};
}
class ChecklyPublicClient {
private client: AxiosInstance;
constructor() {
this.client = createRequestClient({
baseURL: IntegrationUrls.CHECKLY_API_URL,
headers: {
"Content-Type": "application/json"
}
});
}
async send<T>(
connection: TChecklyConnectionConfig,
config: AxiosRequestConfig & { accountId?: string },
retryAttempt = 0
): Promise<T | undefined> {
const response = await this.client.request<T>({
...config,
timeout: 1000 * 60, // 60 seconds timeout
validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests,
headers: getChecklyAuthHeaders(connection, config.accountId)
});
const limiter = getChecklyRatelimiter(response);
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
await limiter.wait();
return this.send(connection, config, retryAttempt + 1);
}
return response.data;
}
healthcheck(connection: TChecklyConnectionConfig) {
switch (connection.method) {
case ChecklyConnectionMethod.ApiKey:
return this.getChecklyAccounts(connection);
default:
throw new Error(`Unsupported Checkly connection method`);
}
}
async getVariables(connection: TChecklyConnectionConfig, accountId: string, limit: number = 50, page: number = 1) {
const res = await this.send<TChecklyVariable[]>(connection, {
accountId,
method: "GET",
url: `/v1/variables`,
params: {
limit,
page
}
});
return res;
}
async createVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "POST",
url: `/v1/variables`,
data: variable
});
return res;
}
async updateVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "PUT",
url: `/v1/variables/${variable.key}`,
data: variable
});
return res;
}
async getVariable(connection: TChecklyConnectionConfig, accountId: string, variable: Pick<TChecklyVariable, "key">) {
try {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "GET",
url: `/v1/variables/${variable.key}`
});
return res;
} catch (error) {
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
return null;
}
throw error;
}
}
async upsertVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) {
const res = await this.getVariable(connection, accountId, variable);
if (!res) {
return this.createVariable(connection, accountId, variable);
}
await this.updateVariable(connection, accountId, variable);
return res;
}
async deleteVariable(
connection: TChecklyConnectionConfig,
accountId: string,
variable: Pick<TChecklyVariable, "key">
) {
try {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "DELETE",
url: `/v1/variables/${variable.key}`
});
return res;
} catch (error) {
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
return null;
}
throw error;
}
}
async getChecklyAccounts(connection: TChecklyConnectionConfig) {
// This endpoint is in beta and might be subject to changes
// Refer: https://developers.checklyhq.com/reference/getv1accounts
const res = await this.send<TChecklyAccount[]>(connection, {
method: "GET",
url: `/v1/accounts`
});
return res;
}
}
export const ChecklyPublicAPI = new ChecklyPublicClient();
@@ -0,0 +1,62 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
export const ChecklyConnectionMethodSchema = z
.nativeEnum(ChecklyConnectionMethod)
.describe(AppConnections.CREATE(AppConnection.Checkly).method);
export const ChecklyConnectionAccessTokenCredentialsSchema = z.object({
apiKey: z.string().trim().min(1, "API Key required").max(255).describe(AppConnections.CREDENTIALS.CHECKLY.apiKey)
});
const BaseChecklyConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Checkly)
});
export const ChecklyConnectionSchema = BaseChecklyConnectionSchema.extend({
method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema
});
export const SanitizedChecklyConnectionSchema = z.discriminatedUnion("method", [
BaseChecklyConnectionSchema.extend({
method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateChecklyConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Checkly).credentials
)
})
]);
export const CreateChecklyConnectionSchema = ValidateChecklyConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Checkly)
);
export const UpdateChecklyConnectionSchema = z
.object({
credentials: ChecklyConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Checkly).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Checkly));
export const ChecklyConnectionListItemSchema = z.object({
name: z.literal("Checkly"),
app: z.literal(AppConnection.Checkly),
methods: z.nativeEnum(ChecklyConnectionMethod).array()
});
@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { ChecklyPublicAPI } from "./checkly-connection-public-client";
import { TChecklyConnection } from "./checkly-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TChecklyConnection>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const checklyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listAccounts = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Checkly, connectionId, actor);
try {
const accounts = await ChecklyPublicAPI.getChecklyAccounts(appConnection);
return accounts!;
} catch (error) {
logger.error(error, "Failed to list accounts on Checkly");
return [];
}
};
return {
listAccounts
};
};
@@ -0,0 +1,35 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
ChecklyConnectionSchema,
CreateChecklyConnectionSchema,
ValidateChecklyConnectionCredentialsSchema
} from "./checkly-connection-schemas";
export type TChecklyConnection = z.infer<typeof ChecklyConnectionSchema>;
export type TChecklyConnectionInput = z.infer<typeof CreateChecklyConnectionSchema> & {
app: AppConnection.Checkly;
};
export type TValidateChecklyConnectionCredentialsSchema = typeof ValidateChecklyConnectionCredentialsSchema;
export type TChecklyConnectionConfig = DiscriminativePick<TChecklyConnection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TChecklyVariable = {
key: string;
value: string;
locked: boolean;
secret: boolean;
};
export type TChecklyAccount = {
id: string;
name: string;
runtimeId: string;
};
@@ -0,0 +1,4 @@
export * from "./checkly-connection-constants";
export * from "./checkly-connection-fns";
export * from "./checkly-connection-schemas";
export * from "./checkly-connection-types";
@@ -145,12 +145,20 @@ export const getGitHubEnvironments = async (appConnection: TGitHubConnection, ow
};
type TokenRespData = {
access_token: string;
access_token?: string;
scope: string;
token_type: string;
error?: string;
};
function isErrorResponse(data: TokenRespData): data is TokenRespData & {
error: string;
error_description: string;
error_uri: string;
} {
return "error" in data;
}
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config;
@@ -198,7 +206,17 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
"Accept-Encoding": "application/json"
}
});
if (isErrorResponse(tokenResp?.data)) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
});
}
} catch (e: unknown) {
if (e instanceof BadRequestError) {
throw e;
}
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
@@ -211,6 +229,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
if (method === GitHubConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{
installations: {
id: number;
@@ -239,10 +261,6 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubConnectionMethod.App:
return {
@@ -0,0 +1,4 @@
export * from "./supabase-connection-constants";
export * from "./supabase-connection-fns";
export * from "./supabase-connection-schemas";
export * from "./supabase-connection-types";
@@ -0,0 +1,3 @@
export enum SupabaseConnectionMethod {
AccessToken = "access-token"
}
@@ -0,0 +1,58 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
import { SupabasePublicAPI } from "./supabase-connection-public-client";
import { TSupabaseConnection, TSupabaseConnectionConfig } from "./supabase-connection-types";
export const getSupabaseConnectionListItem = () => {
return {
name: "Supabase" as const,
app: AppConnection.Supabase as const,
methods: Object.values(SupabaseConnectionMethod)
};
};
export const validateSupabaseConnectionCredentials = async (config: TSupabaseConnectionConfig) => {
const { credentials } = config;
try {
await SupabasePublicAPI.healthcheck(config);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection - verify credentials"
});
}
return credentials;
};
export const listProjects = async (appConnection: TSupabaseConnection) => {
try {
return await SupabasePublicAPI.getProjects(appConnection);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list projects: ${error.message || "Unknown error"}`
});
}
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: "Unable to list projects",
error
});
}
};
@@ -0,0 +1,133 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable class-methods-use-this */
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode } from "axios";
import { createRequestClient } from "@app/lib/config/request";
import { delay } from "@app/lib/delay";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
import { TSupabaseConnectionConfig, TSupabaseProject, TSupabaseSecret } from "./supabase-connection-types";
export const getSupabaseInstanceUrl = async (config: TSupabaseConnectionConfig) => {
const instanceUrl = config.credentials.instanceUrl
? removeTrailingSlash(config.credentials.instanceUrl)
: "https://api.supabase.com";
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export function getSupabaseAuthHeaders(connection: TSupabaseConnectionConfig): Record<string, string> {
switch (connection.method) {
case SupabaseConnectionMethod.AccessToken:
return {
Authorization: `Bearer ${connection.credentials.accessKey}`
};
default:
throw new Error(`Unsupported Supabase connection method`);
}
}
export function getSupabaseRatelimiter(response: AxiosResponse): {
maxAttempts: number;
isRatelimited: boolean;
wait: () => Promise<void>;
} {
const wait = () => {
return delay(60 * 1000);
};
return {
isRatelimited: response.status === HttpStatusCode.TooManyRequests,
wait,
maxAttempts: 3
};
}
class SupabasePublicClient {
private client: AxiosInstance;
constructor() {
this.client = createRequestClient({
headers: {
"Content-Type": "application/json"
}
});
}
async send<T>(
connection: TSupabaseConnectionConfig,
config: AxiosRequestConfig,
retryAttempt = 0
): Promise<T | undefined> {
const response = await this.client.request<T>({
...config,
baseURL: await getSupabaseInstanceUrl(connection),
validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests,
headers: getSupabaseAuthHeaders(connection)
});
const limiter = getSupabaseRatelimiter(response);
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
await limiter.wait();
return this.send(connection, config, retryAttempt + 1);
}
return response.data;
}
async healthcheck(connection: TSupabaseConnectionConfig) {
switch (connection.method) {
case SupabaseConnectionMethod.AccessToken:
return void (await this.getProjects(connection));
default:
throw new Error(`Unsupported Supabase connection method`);
}
}
async getVariables(connection: TSupabaseConnectionConfig, projectRef: string) {
const res = await this.send<TSupabaseSecret[]>(connection, {
method: "GET",
url: `/v1/projects/${projectRef}/secrets`
});
return res;
}
// Supabase does not support updating variables directly
// Instead, just call create again with the same key and it will overwrite the existing variable
async createVariables(connection: TSupabaseConnectionConfig, projectRef: string, ...variables: TSupabaseSecret[]) {
const res = await this.send<TSupabaseSecret>(connection, {
method: "POST",
url: `/v1/projects/${projectRef}/secrets`,
data: variables
});
return res;
}
async deleteVariables(connection: TSupabaseConnectionConfig, projectRef: string, ...variables: string[]) {
const res = await this.send(connection, {
method: "DELETE",
url: `/v1/projects/${projectRef}/secrets`,
data: variables
});
return res;
}
async getProjects(connection: TSupabaseConnectionConfig) {
const res = await this.send<TSupabaseProject[]>(connection, {
method: "GET",
url: `/v1/projects`
});
return res;
}
}
export const SupabasePublicAPI = new SupabasePublicClient();
@@ -0,0 +1,70 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
export const SupabaseConnectionMethodSchema = z
.nativeEnum(SupabaseConnectionMethod)
.describe(AppConnections.CREATE(AppConnection.Supabase).method);
export const SupabaseConnectionAccessTokenCredentialsSchema = z.object({
accessKey: z
.string()
.trim()
.min(1, "Access Key required")
.max(255)
.describe(AppConnections.CREDENTIALS.SUPABASE.accessKey),
instanceUrl: z.string().trim().url().max(255).describe(AppConnections.CREDENTIALS.SUPABASE.instanceUrl).optional()
});
const BaseSupabaseConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Supabase)
});
export const SupabaseConnectionSchema = BaseSupabaseConnectionSchema.extend({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema
});
export const SanitizedSupabaseConnectionSchema = z.discriminatedUnion("method", [
BaseSupabaseConnectionSchema.extend({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateSupabaseConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Supabase).credentials
)
})
]);
export const CreateSupabaseConnectionSchema = ValidateSupabaseConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Supabase)
);
export const UpdateSupabaseConnectionSchema = z
.object({
credentials: SupabaseConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Supabase).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Supabase));
export const SupabaseConnectionListItemSchema = z.object({
name: z.literal("Supabase"),
app: z.literal(AppConnection.Supabase),
methods: z.nativeEnum(SupabaseConnectionMethod).array()
});
@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listProjects as getSupabaseProjects } from "./supabase-connection-fns";
import { TSupabaseConnection } from "./supabase-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TSupabaseConnection>;
export const supabaseConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Supabase, connectionId, actor);
try {
const projects = await getSupabaseProjects(appConnection);
return projects ?? [];
} catch (error) {
logger.error(error, "Failed to establish connection with Supabase");
return [];
}
};
return {
listProjects
};
};
@@ -0,0 +1,44 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateSupabaseConnectionSchema,
SupabaseConnectionSchema,
ValidateSupabaseConnectionCredentialsSchema
} from "./supabase-connection-schemas";
export type TSupabaseConnection = z.infer<typeof SupabaseConnectionSchema>;
export type TSupabaseConnectionInput = z.infer<typeof CreateSupabaseConnectionSchema> & {
app: AppConnection.Supabase;
};
export type TValidateSupabaseConnectionCredentialsSchema = typeof ValidateSupabaseConnectionCredentialsSchema;
export type TSupabaseConnectionConfig = DiscriminativePick<TSupabaseConnection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TSupabaseProject = {
id: string;
organization_id: string;
name: string;
region: string;
created_at: Date;
status: string;
database: TSupabaseDatabase;
};
type TSupabaseDatabase = {
host: string;
version: string;
postgres_engine: string;
release_channel: string;
};
export type TSupabaseSecret = {
name: string;
value: string;
};
@@ -218,7 +218,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
};
const findWithAssociatedCa = async (
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string },
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string },
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
tx?: Knex
) => {
@@ -1068,11 +1068,11 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate chain" });
const parentCertObj = chainItems[1];
const parentCertSubject = parentCertObj.subject;
const parentSerialNumber = parentCertObj.serialNumber;
const [parentCa] = await certificateAuthorityDAL.findWithAssociatedCa({
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: ca.projectId,
[`${TableName.InternalCertificateAuthority}.dn` as "dn"]: parentCertSubject
[`${TableName.InternalCertificateAuthority}.serialNumber` as "serialNumber"]: parentSerialNumber
});
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
@@ -6,7 +6,8 @@ export type TLoginOciAuthDTO = {
headers: {
authorization: string;
host: string;
"x-date": string;
"x-date"?: string;
date?: string;
};
};
@@ -174,6 +174,7 @@ export const fnSecretsV2FromImports = async ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => Promise<string | undefined>;
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => {
@@ -293,7 +294,8 @@ export const fnSecretsV2FromImports = async ({
value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath,
environment: processedImport.environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const CHECKLY_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Checkly",
destination: SecretSync.Checkly,
connection: AppConnection.Checkly,
canImportSecrets: false
};
@@ -0,0 +1,102 @@
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ChecklyPublicAPI } from "@app/services/app-connection/checkly/checkly-connection-public-client";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { TSecretMap } from "../secret-sync-types";
import { TChecklySyncWithCredentials } from "./checkly-sync-types";
export const ChecklySyncFns = {
async getSecrets(secretSync: TChecklySyncWithCredentials) {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
async syncSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const config = secretSync.destinationConfig;
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
for await (const key of Object.keys(secretMap)) {
try {
const entry = secretMap[key];
// If value is empty, we skip the upsert - checkly does not allow empty values
if (entry.value.trim() === "") {
// Delete the secret from Checkly if its empty
if (!disableSecretDeletion) {
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
key
});
}
continue; // Skip empty values
}
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
key,
value: entry.value,
secret: true,
locked: true
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (disableSecretDeletion) return;
for await (const key of Object.keys(checklySecrets)) {
try {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
if (!secretMap[key]) {
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
key
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
},
async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
const config = secretSync.destinationConfig;
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
for await (const secret of Object.keys(checklySecrets)) {
try {
if (secret in secretMap) {
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
key: secret
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: secret
});
}
}
}
};
@@ -0,0 +1,43 @@
import { z } from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const ChecklySyncDestinationConfigSchema = z.object({
accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
accountName: z.string().min(1, "Account Name is required").max(255, "Account ID must be less than 255 characters")
});
const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const ChecklySyncSchema = BaseSecretSyncSchema(SecretSync.Checkly, ChecklySyncOptionsConfig).extend({
destination: z.literal(SecretSync.Checkly),
destinationConfig: ChecklySyncDestinationConfigSchema
});
export const CreateChecklySyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Checkly,
ChecklySyncOptionsConfig
).extend({
destinationConfig: ChecklySyncDestinationConfigSchema
});
export const UpdateChecklySyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Checkly,
ChecklySyncOptionsConfig
).extend({
destinationConfig: ChecklySyncDestinationConfigSchema.optional()
});
export const ChecklySyncListItemSchema = z.object({
name: z.literal("Checkly"),
connection: z.literal(AppConnection.Checkly),
destination: z.literal(SecretSync.Checkly),
canImportSecrets: z.literal(false)
});
@@ -0,0 +1,23 @@
import z from "zod";
import { TChecklyConnection, TChecklyVariable } from "@app/services/app-connection/checkly";
import { ChecklySyncListItemSchema, ChecklySyncSchema, CreateChecklySyncSchema } from "./checkly-sync-schemas";
export type TChecklySyncListItem = z.infer<typeof ChecklySyncListItemSchema>;
export type TChecklySync = z.infer<typeof ChecklySyncSchema>;
export type TChecklySyncInput = z.infer<typeof CreateChecklySyncSchema>;
export type TChecklySyncWithCredentials = TChecklySync & {
connection: TChecklyConnection;
};
export type TChecklySecret = TChecklyVariable;
export type TChecklyVariablesGraphResponse = {
data: {
variables: Record<string, string>;
};
};
@@ -22,9 +22,10 @@ export enum SecretSync {
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages",
CloudflareWorkers = "cloudflare-workers",
Supabase = "supabase",
Zabbix = "zabbix",
Railway = "railway"
Railway = "railway",
Checkly = "checkly"
}
export enum SecretSyncInitialSyncBehavior {
@@ -29,6 +29,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact
import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops";
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants";
import { ChecklySyncFns } from "./checkly/checkly-sync-fns";
import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers";
@@ -44,6 +46,7 @@ import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
import { RailwaySyncFns } from "./railway/railway-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps";
import { SUPABASE_SYNC_LIST_OPTION, SupabaseSyncFns } from "./supabase";
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
@@ -74,9 +77,10 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION,
[SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION,
[SecretSync.Supabase]: SUPABASE_SYNC_LIST_OPTION,
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION,
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION,
[SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -250,6 +254,10 @@ export const SecretSyncFns = {
return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway:
return RailwaySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Checkly:
return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -351,6 +359,12 @@ export const SecretSyncFns = {
case SecretSync.Railway:
secretMap = await RailwaySyncFns.getSecrets(secretSync);
break;
case SecretSync.Checkly:
secretMap = await ChecklySyncFns.getSecrets(secretSync);
break;
case SecretSync.Supabase:
secretMap = await SupabaseSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -434,6 +448,10 @@ export const SecretSyncFns = {
return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway:
return RailwaySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Checkly:
return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -25,9 +25,10 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages",
[SecretSync.CloudflareWorkers]: "Cloudflare Workers",
[SecretSync.Supabase]: "Supabase",
[SecretSync.Zabbix]: "Zabbix",
[SecretSync.Railway]: "Railway"
[SecretSync.Railway]: "Railway",
[SecretSync.Checkly]: "Checkly"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -54,9 +55,10 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Supabase]: AppConnection.Supabase,
[SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway
[SecretSync.Railway]: AppConnection.Railway,
[SecretSync.Checkly]: AppConnection.Checkly
};
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
@@ -83,7 +85,8 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular,
[SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular,
[SecretSync.Supabase]: SecretSyncPlanType.Regular,
[SecretSync.Zabbix]: SecretSyncPlanType.Regular,
[SecretSync.Railway]: SecretSyncPlanType.Regular
[SecretSync.Railway]: SecretSyncPlanType.Regular,
[SecretSync.Checkly]: SecretSyncPlanType.Regular
};
@@ -231,7 +231,8 @@ export const secretSyncQueueFactory = ({
environment: environment.slug,
secretPath: folder.path,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
secretMap[secretKey] = { value: expandedSecretValue || "" };
@@ -72,6 +72,12 @@ import {
TAzureKeyVaultSyncListItem,
TAzureKeyVaultSyncWithCredentials
} from "./azure-key-vault";
import {
TChecklySync,
TChecklySyncInput,
TChecklySyncListItem,
TChecklySyncWithCredentials
} from "./checkly/checkly-sync-types";
import {
TCloudflarePagesSync,
TCloudflarePagesSyncInput,
@@ -112,6 +118,12 @@ import {
TRenderSyncListItem,
TRenderSyncWithCredentials
} from "./render/render-sync-types";
import {
TSupabaseSync,
TSupabaseSyncInput,
TSupabaseSyncListItem,
TSupabaseSyncWithCredentials
} from "./supabase/supabase-sync-types";
import {
TTeamCitySync,
TTeamCitySyncInput,
@@ -152,7 +164,9 @@ export type TSecretSync =
| TCloudflarePagesSync
| TCloudflareWorkersSync
| TZabbixSync
| TRailwaySync;
| TRailwaySync
| TChecklySync
| TSupabaseSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -179,7 +193,9 @@ export type TSecretSyncWithCredentials =
| TCloudflarePagesSyncWithCredentials
| TCloudflareWorkersSyncWithCredentials
| TZabbixSyncWithCredentials
| TRailwaySyncWithCredentials;
| TRailwaySyncWithCredentials
| TChecklySyncWithCredentials
| TSupabaseSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -206,7 +222,9 @@ export type TSecretSyncInput =
| TCloudflarePagesSyncInput
| TCloudflareWorkersSyncInput
| TZabbixSyncInput
| TRailwaySyncInput;
| TRailwaySyncInput
| TChecklySyncInput
| TSupabaseSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -233,7 +251,9 @@ export type TSecretSyncListItem =
| TCloudflarePagesSyncListItem
| TCloudflareWorkersSyncListItem
| TZabbixSyncListItem
| TRailwaySyncListItem;
| TRailwaySyncListItem
| TChecklySyncListItem
| TSupabaseSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;
@@ -0,0 +1,4 @@
export * from "./supabase-sync-constants";
export * from "./supabase-sync-fns";
export * from "./supabase-sync-schemas";
export * from "./supabase-sync-types";
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const SUPABASE_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Supabase",
destination: SecretSync.Supabase,
connection: AppConnection.Supabase,
canImportSecrets: false
};
@@ -0,0 +1,102 @@
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { chunkArray } from "@app/lib/fn";
import { TSupabaseSecret } from "@app/services/app-connection/supabase";
import { SupabasePublicAPI } from "@app/services/app-connection/supabase/supabase-connection-public-client";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { TSecretMap } from "../secret-sync-types";
import { TSupabaseSyncWithCredentials } from "./supabase-sync-types";
const SUPABASE_INTERNAL_SECRETS = ["SUPABASE_URL", "SUPABASE_ANON_KEY", "SUPABASE_SERVICE_ROLE_KEY", "SUPABASE_DB_URL"];
export const SupabaseSyncFns = {
async getSecrets(secretSync: TSupabaseSyncWithCredentials) {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
async syncSecrets(secretSync: TSupabaseSyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const config = secretSync.destinationConfig;
const variables = await SupabasePublicAPI.getVariables(secretSync.connection, config.projectId);
const supabaseSecrets = new Map(variables!.map((variable) => [variable.name, variable]));
const toCreate: TSupabaseSecret[] = [];
for (const key of Object.keys(secretMap)) {
const variable: TSupabaseSecret = { name: key, value: secretMap[key].value ?? "" };
toCreate.push(variable);
}
for await (const batch of chunkArray(toCreate, 100)) {
try {
await SupabasePublicAPI.createVariables(secretSync.connection, config.projectId, ...batch);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: batch[0].name // Use the first key in the batch for error reporting
});
}
}
if (disableSecretDeletion) return;
const toDelete: string[] = [];
for (const key of supabaseSecrets.keys()) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", keySchema) || SUPABASE_INTERNAL_SECRETS.includes(key)) continue;
if (!secretMap[key]) {
toDelete.push(key);
}
}
for await (const batch of chunkArray(toDelete, 100)) {
try {
await SupabasePublicAPI.deleteVariables(secretSync.connection, config.projectId, ...batch);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: batch[0] // Use the first key in the batch for error reporting
});
}
}
},
async removeSecrets(secretSync: TSupabaseSyncWithCredentials, secretMap: TSecretMap) {
const config = secretSync.destinationConfig;
const variables = await SupabasePublicAPI.getVariables(secretSync.connection, config.projectId);
const supabaseSecrets = new Map(variables!.map((variable) => [variable.name, variable]));
const toDelete: string[] = [];
for (const key of supabaseSecrets.keys()) {
if (SUPABASE_INTERNAL_SECRETS.includes(key) || !(key in secretMap)) continue;
toDelete.push(key);
}
for await (const batch of chunkArray(toDelete, 100)) {
try {
await SupabasePublicAPI.deleteVariables(secretSync.connection, config.projectId, ...batch);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: batch[0] // Use the first key in the batch for error reporting
});
}
}
}
};
@@ -0,0 +1,43 @@
import { z } from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const SupabaseSyncDestinationConfigSchema = z.object({
projectId: z.string().max(255).min(1, "Project ID is required"),
projectName: z.string().max(255).min(1, "Project Name is required")
});
const SupabaseSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const SupabaseSyncSchema = BaseSecretSyncSchema(SecretSync.Supabase, SupabaseSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Supabase),
destinationConfig: SupabaseSyncDestinationConfigSchema
});
export const CreateSupabaseSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Supabase,
SupabaseSyncOptionsConfig
).extend({
destinationConfig: SupabaseSyncDestinationConfigSchema
});
export const UpdateSupabaseSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Supabase,
SupabaseSyncOptionsConfig
).extend({
destinationConfig: SupabaseSyncDestinationConfigSchema.optional()
});
export const SupabaseSyncListItemSchema = z.object({
name: z.literal("Supabase"),
connection: z.literal(AppConnection.Supabase),
destination: z.literal(SecretSync.Supabase),
canImportSecrets: z.literal(false)
});
@@ -0,0 +1,21 @@
import z from "zod";
import { TSupabaseConnection } from "@app/services/app-connection/supabase";
import { CreateSupabaseSyncSchema, SupabaseSyncListItemSchema, SupabaseSyncSchema } from "./supabase-sync-schemas";
export type TSupabaseSyncListItem = z.infer<typeof SupabaseSyncListItemSchema>;
export type TSupabaseSync = z.infer<typeof SupabaseSyncSchema>;
export type TSupabaseSyncInput = z.infer<typeof CreateSupabaseSyncSchema>;
export type TSupabaseSyncWithCredentials = TSupabaseSync & {
connection: TSupabaseConnection;
};
export type TSupabaseVariablesGraphResponse = {
data: {
variables: Record<string, string>;
};
};
@@ -614,6 +614,7 @@ export const expandSecretReferencesFactory = ({
secretPath: string;
environment: string;
shouldStackTrace?: boolean;
secretKey: string;
}) => {
const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode;
@@ -656,7 +657,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretKey}' in environment '${environment}' at path '${secretPath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(environment, secretPath);
@@ -675,7 +676,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretReferenceKey}' in environment '${secretReferenceEnvironment}' at path '${secretReferencePath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@@ -692,6 +693,7 @@ export const expandSecretReferencesFactory = ({
secretPath: referencedSecretPath,
environment: referencedSecretEnvironmentSlug,
depth: depth + 1,
secretKey: referencedSecretKey,
trace
};
@@ -726,6 +728,7 @@ export const expandSecretReferencesFactory = ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
@@ -741,6 +744,7 @@ export const expandSecretReferencesFactory = ({
value?: string;
secretPath: string;
environment: string;
secretKey: string;
}) => {
const { stackTrace, expandedValue } = await recursivelyExpandSecret({ ...inputSecret, shouldStackTrace: true });
return { stackTrace, expandedValue };
@@ -1105,7 +1105,7 @@ export const secretV2BridgeServiceFactory = ({
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
await Promise.allSettled(
const settledPromises = await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
@@ -1113,7 +1113,8 @@ export const secretV2BridgeServiceFactory = ({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
@@ -1121,6 +1122,35 @@ export const secretV2BridgeServiceFactory = ({
)
)
);
const errors: { path: string; error: string }[] = [];
settledPromises.forEach((outerResult: PromiseSettledResult<PromiseSettledResult<void>[]>, outerIndex) => {
const groupedPath = Object.keys(secretsGroupByPath)[outerIndex];
if (outerResult.status === "rejected") {
errors.push({
path: groupedPath,
error: `Failed to process secret group: ${outerResult.reason}`
});
} else {
// Check inner promise results
outerResult.value.forEach((innerResult: PromiseSettledResult<void>) => {
if (innerResult.status === "rejected") {
const reason = innerResult.reason as ForbiddenRequestError;
errors.push({
path: groupedPath,
error: reason.message
});
}
});
}
});
if (errors.length > 0) {
throw new ForbiddenRequestError({
message: "Failed to expand one or more secret references",
details: errors.map((err) => err.error)
});
}
}
if (!includeImports) {
@@ -1424,7 +1454,8 @@ export const secretV2BridgeServiceFactory = ({
environment,
secretPath: path,
value: secretValue,
skipMultilineEncoding: secret.skipMultilineEncoding
skipMultilineEncoding: secret.skipMultilineEncoding,
secretKey: secret.key
});
secretValue = expandedSecretValue || "";
@@ -2722,7 +2753,8 @@ export const secretV2BridgeServiceFactory = ({
const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({
environment,
secretPath,
value: decryptedSecretValue
value: decryptedSecretValue,
secretKey: secretName
});
return { tree: stackTrace, value: expandedValue };
+2 -1
View File
@@ -426,7 +426,8 @@ export const secretQueueFactory = ({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
content[secretKey] = { value: expandedSecretValue || "" };