Compare commits
30 Commits
access-req
...
fix/cliExp
Author | SHA1 | Date | |
---|---|---|---|
350afee45e | |||
8187b1da91 | |||
fd761df8e5 | |||
6ce6c276cd | |||
32b2f7b0fe | |||
4c2823c480 | |||
60438694e4 | |||
fdaf8f9a87 | |||
c1798d37be | |||
01c6d3192d | |||
d6c2789d46 | |||
58ba0c8ed4 | |||
f38c574030 | |||
c330d8ca8a | |||
2cb0ecc768 | |||
ecc15bb432 | |||
0e07ebae7b | |||
a94a26263a | |||
b4ef55db4e | |||
307b5d1f87 | |||
54087038c2 | |||
f835bf0ba8 | |||
c79ea0631e | |||
948799822f | |||
c14a431177 | |||
7ef077228e | |||
023079be16 | |||
f95bcabef7 | |||
c4e08b9811 | |||
7784b8a81c |
@ -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(
|
||||
{
|
||||
|
@ -2282,6 +2282,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."
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -2488,6 +2495,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
|
||||
});
|
@ -30,7 +30,9 @@ export enum AppConnection {
|
||||
Cloudflare = "cloudflare",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
Bitbucket = "bitbucket"
|
||||
Bitbucket = "bitbucket",
|
||||
Checkly = "checkly",
|
||||
Supabase = "supabase"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@ -56,6 +56,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,
|
||||
@ -94,6 +95,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,
|
||||
@ -146,7 +152,9 @@ export const listAppConnectionOptions = () => {
|
||||
getCloudflareConnectionListItem(),
|
||||
getZabbixConnectionListItem(),
|
||||
getRailwayConnectionListItem(),
|
||||
getBitbucketConnectionListItem()
|
||||
getBitbucketConnectionListItem(),
|
||||
getChecklyConnectionListItem(),
|
||||
getSupabaseConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@ -229,7 +237,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);
|
||||
@ -287,7 +297,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}`);
|
||||
@ -350,7 +363,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
|
||||
};
|
||||
|
@ -49,6 +49,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";
|
||||
@ -76,6 +78,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";
|
||||
@ -128,7 +132,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 = ({
|
||||
@ -541,6 +547,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)
|
||||
};
|
||||
};
|
||||
|
@ -68,6 +68,12 @@ import {
|
||||
TCamundaConnectionInput,
|
||||
TValidateCamundaConnectionCredentialsSchema
|
||||
} from "./camunda";
|
||||
import {
|
||||
TChecklyConnection,
|
||||
TChecklyConnectionConfig,
|
||||
TChecklyConnectionInput,
|
||||
TValidateChecklyConnectionCredentialsSchema
|
||||
} from "./checkly";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
@ -153,6 +159,12 @@ import {
|
||||
TRenderConnectionInput,
|
||||
TValidateRenderConnectionCredentialsSchema
|
||||
} from "./render/render-connection-types";
|
||||
import {
|
||||
TSupabaseConnection,
|
||||
TSupabaseConnectionConfig,
|
||||
TSupabaseConnectionInput,
|
||||
TValidateSupabaseConnectionCredentialsSchema
|
||||
} from "./supabase";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
@ -217,6 +229,8 @@ export type TAppConnection = { id: string } & (
|
||||
| TBitbucketConnection
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
| TSupabaseConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@ -256,6 +270,8 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TBitbucketConnectionInput
|
||||
| TZabbixConnectionInput
|
||||
| TRailwayConnectionInput
|
||||
| TChecklyConnectionInput
|
||||
| TSupabaseConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@ -302,7 +318,9 @@ export type TAppConnectionConfig =
|
||||
| TCloudflareConnectionConfig
|
||||
| TBitbucketConnectionConfig
|
||||
| TZabbixConnectionConfig
|
||||
| TRailwayConnectionConfig;
|
||||
| TRailwayConnectionConfig
|
||||
| TChecklyConnectionConfig
|
||||
| TSupabaseConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@ -336,7 +354,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;
|
||||
};
|
4
backend/src/services/app-connection/checkly/index.ts
Normal file
@ -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 {
|
||||
|
4
backend/src/services/app-connection/supabase/index.ts
Normal file
@ -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;
|
||||
};
|
@ -6,7 +6,8 @@ export type TLoginOciAuthDTO = {
|
||||
headers: {
|
||||
authorization: string;
|
||||
host: string;
|
||||
"x-date": string;
|
||||
"x-date"?: string;
|
||||
date?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
};
|
102
backend/src/services/secret-sync/checkly/checkly-sync-fns.ts
Normal file
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
4
backend/src/services/secret-sync/supabase/index.ts
Normal file
@ -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
|
||||
};
|
102
backend/src/services/secret-sync/supabase/supabase-sync-fns.ts
Normal file
@ -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>;
|
||||
};
|
||||
};
|
@ -33,6 +33,7 @@ Every feature/problem is unique, but your design docs should generally include t
|
||||
- A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs).
|
||||
3. **Context**
|
||||
- Explain the problem's background, why it's important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.).
|
||||
- Consider whether this feature has notable sales implications (e.g., affects pricing, customer commitments, go-to-market strategy, or competitive positioning) that would require Sales team input and approval.
|
||||
4. **Solution**
|
||||
|
||||
- Provide a big-picture explanation of the solution, followed by detailed technical architecture.
|
||||
@ -76,3 +77,11 @@ Before sharing your design docs with others, review your design doc as if you we
|
||||
- Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up.
|
||||
4. **Team Review and Feedback**
|
||||
- Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on.
|
||||
5. **Sales Approval (When Applicable)**
|
||||
- If your design document has notable sales implications, get explicit approval from the Sales team before proceeding to implementation. This includes features that:
|
||||
- Affect pricing models or billing structures
|
||||
- Impact customer commitments or contractual obligations
|
||||
- Change core product functionality that's actively being sold
|
||||
- Introduce new capabilities that could affect competitive positioning
|
||||
- Modify user experience in ways that could impact customer acquisition or retention
|
||||
- Share the design document with the Sales team to ensure alignment between the proposed technical approach and sales strategy, pricing models, and market positioning.
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/checkly/available"
|
||||
---
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/checkly"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Checkly Connections](/integrations/app-connections/checkly) to learn how to obtain the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/checkly/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/checkly/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/checkly/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/checkly"
|
||||
---
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/checkly/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Checkly Connections](/integrations/app-connections/checkly) to learn how to obtain the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/supabase/available"
|
||||
---
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/supabase"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Supabase Connections](/integrations/app-connections/supabase) to learn how to obtain the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/supabase/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/supabase/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/supabase/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/supabase"
|
||||
---
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/supabase/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Supabase Connections](/integrations/app-connections/supabase) to learn how to obtain the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/checkly"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/checkly/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/checkly/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/checkly/sync-name/{syncName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/checkly"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/checkly/{syncId}/remove-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/checkly/{syncId}/sync-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/checkly/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/supabase"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/supabase/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/supabase/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/supabase/sync-name/{syncName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/supabase"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/supabase/{syncId}/remove-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/supabase/{syncId}/sync-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/supabase/{syncId}"
|
||||
---
|
@ -2,3 +2,8 @@
|
||||
title: "Login"
|
||||
openapi: "POST /api/v1/auth/tls-cert-auth/login"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Infisical US/EU and dedicated instances are deployed with AWS ALB. TLS Certificate Auth must flow through our ALB mTLS pass-through in order to authenticate.
|
||||
When you are authenticating with TLS Certificate Auth, you must use the port `8443` instead of the default `443`. Example: `https://app.infisical.com:8443/api/v1/auth/tls-cert-auth/login`
|
||||
</Warning>
|
@ -9,7 +9,7 @@ infisical export [options]
|
||||
|
||||
## Description
|
||||
|
||||
Export environment variables from the platform into a file format.
|
||||
Export environment variables from the platform into a file format. By default, output is sent to stdout (standard output), but you can use the `--output-file` flag to save directly to a file.
|
||||
|
||||
## Subcommands & flags
|
||||
|
||||
@ -21,18 +21,19 @@ $ infisical export
|
||||
|
||||
# Export variables to a .env file
|
||||
infisical export > .env
|
||||
infisical export --output-file=./.env
|
||||
|
||||
# Export variables to a .env file (with export keyword)
|
||||
infisical export --format=dotenv-export > .env
|
||||
|
||||
# Export variables to a CSV file
|
||||
infisical export --format=csv > secrets.csv
|
||||
infisical export --format=dotenv-export --output-file=./.env
|
||||
|
||||
# Export variables to a JSON file
|
||||
infisical export --format=json > secrets.json
|
||||
infisical export --format=json --output-file=./secrets.json
|
||||
|
||||
# Export variables to a YAML file
|
||||
infisical export --format=yaml > secrets.yaml
|
||||
infisical export --format=yaml --output-file=./secrets.yaml
|
||||
|
||||
# Render secrets using a custom template file
|
||||
infisical export --template=<path to template>
|
||||
@ -73,6 +74,34 @@ infisical export --template=<path to template>
|
||||
|
||||
### flags
|
||||
|
||||
<Accordion title="--output-file">
|
||||
The path to write the output file to. Can be a full file path, directory, or filename.
|
||||
|
||||
```bash
|
||||
# Export to specific file
|
||||
infisical export --format=json --output-file=./secrets.json
|
||||
|
||||
# Export to directory (uses default filename based on format)
|
||||
infisical export --format=yaml --output-file=./
|
||||
```
|
||||
|
||||
**When `--output-file` is specified:**
|
||||
- Secrets are saved directly to the specified file
|
||||
- A success message is displayed showing the file path
|
||||
- For directories: adds default filename `secrets.{format}` (e.g., `secrets.json`, `secrets.yaml`)
|
||||
- For dotenv formats in directories: uses `.env` as the filename
|
||||
|
||||
**When `--output-file` is NOT specified (default behavior):**
|
||||
- Output is sent to stdout (standard output)
|
||||
- You can use shell redirection like `infisical export > secrets.json`
|
||||
- Maintains backwards compatibility with existing scripts
|
||||
|
||||
<Warning>
|
||||
If you're using shell redirection and your token expires, re-authentication will fail because the prompt can't display properly due to the redirection.
|
||||
</Warning>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--template">
|
||||
The `--template` flag specifies the path to the template file used for rendering secrets. When using templates, you can omit the other format flags.
|
||||
|
||||
@ -94,6 +123,7 @@ infisical export --template=<path to template>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--env">
|
||||
Used to set the environment that secrets are pulled from.
|
||||
|
||||
@ -162,7 +192,7 @@ infisical export --template=<path to template>
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --tags=tag1,tag2,tag3 -- npm run dev
|
||||
infisical export --tags=tag1,tag2,tag3 --env=dev
|
||||
```
|
||||
|
||||
Note: you must reference the tag by its slug name not its fully qualified name. Go to project settings to view all tag slugs.
|
||||
|
@ -78,10 +78,7 @@
|
||||
},
|
||||
{
|
||||
"group": "Infisical SSH",
|
||||
"pages": [
|
||||
"documentation/platform/ssh/overview",
|
||||
"documentation/platform/ssh/host-groups"
|
||||
]
|
||||
"pages": ["documentation/platform/ssh/overview", "documentation/platform/ssh/host-groups"]
|
||||
},
|
||||
{
|
||||
"group": "Key Management (KMS)",
|
||||
@ -378,10 +375,7 @@
|
||||
},
|
||||
{
|
||||
"group": "Architecture",
|
||||
"pages": [
|
||||
"internals/architecture/components",
|
||||
"internals/architecture/cloud"
|
||||
]
|
||||
"pages": ["internals/architecture/components", "internals/architecture/cloud"]
|
||||
},
|
||||
"internals/security",
|
||||
"internals/service-tokens"
|
||||
@ -472,6 +466,7 @@
|
||||
"integrations/app-connections/azure-key-vault",
|
||||
"integrations/app-connections/bitbucket",
|
||||
"integrations/app-connections/camunda",
|
||||
"integrations/app-connections/checkly",
|
||||
"integrations/app-connections/cloudflare",
|
||||
"integrations/app-connections/databricks",
|
||||
"integrations/app-connections/flyio",
|
||||
@ -490,6 +485,7 @@
|
||||
"integrations/app-connections/postgres",
|
||||
"integrations/app-connections/railway",
|
||||
"integrations/app-connections/render",
|
||||
"integrations/app-connections/supabase",
|
||||
"integrations/app-connections/teamcity",
|
||||
"integrations/app-connections/terraform-cloud",
|
||||
"integrations/app-connections/vercel",
|
||||
@ -513,6 +509,7 @@
|
||||
"integrations/secret-syncs/azure-devops",
|
||||
"integrations/secret-syncs/azure-key-vault",
|
||||
"integrations/secret-syncs/camunda",
|
||||
"integrations/secret-syncs/checkly",
|
||||
"integrations/secret-syncs/cloudflare-pages",
|
||||
"integrations/secret-syncs/cloudflare-workers",
|
||||
"integrations/secret-syncs/databricks",
|
||||
@ -526,6 +523,7 @@
|
||||
"integrations/secret-syncs/oci-vault",
|
||||
"integrations/secret-syncs/railway",
|
||||
"integrations/secret-syncs/render",
|
||||
"integrations/secret-syncs/supabase",
|
||||
"integrations/secret-syncs/teamcity",
|
||||
"integrations/secret-syncs/terraform-cloud",
|
||||
"integrations/secret-syncs/vercel",
|
||||
@ -553,10 +551,7 @@
|
||||
"integrations/cloud/gcp-secret-manager",
|
||||
{
|
||||
"group": "Cloudflare",
|
||||
"pages": [
|
||||
"integrations/cloud/cloudflare-pages",
|
||||
"integrations/cloud/cloudflare-workers"
|
||||
]
|
||||
"pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"]
|
||||
},
|
||||
"integrations/cloud/terraform-cloud",
|
||||
"integrations/cloud/databricks",
|
||||
@ -668,11 +663,7 @@
|
||||
"cli/commands/reset",
|
||||
{
|
||||
"group": "infisical scan",
|
||||
"pages": [
|
||||
"cli/commands/scan",
|
||||
"cli/commands/scan-git-changes",
|
||||
"cli/commands/scan-install"
|
||||
]
|
||||
"pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -996,9 +987,7 @@
|
||||
"pages": [
|
||||
{
|
||||
"group": "Kubernetes",
|
||||
"pages": [
|
||||
"api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"
|
||||
]
|
||||
"pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"]
|
||||
},
|
||||
"api-reference/endpoints/dynamic-secrets/create",
|
||||
"api-reference/endpoints/dynamic-secrets/update",
|
||||
@ -1328,6 +1317,17 @@
|
||||
"api-reference/endpoints/app-connections/camunda/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Checkly",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/checkly/list",
|
||||
"api-reference/endpoints/app-connections/checkly/get-by-id",
|
||||
"api-reference/endpoints/app-connections/checkly/get-by-name",
|
||||
"api-reference/endpoints/app-connections/checkly/create",
|
||||
"api-reference/endpoints/app-connections/checkly/update",
|
||||
"api-reference/endpoints/app-connections/checkly/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloudflare",
|
||||
"pages": [
|
||||
@ -1544,6 +1544,18 @@
|
||||
"api-reference/endpoints/app-connections/render/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Supabase",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/supabase/list",
|
||||
"api-reference/endpoints/app-connections/supabase/available",
|
||||
"api-reference/endpoints/app-connections/supabase/get-by-id",
|
||||
"api-reference/endpoints/app-connections/supabase/get-by-name",
|
||||
"api-reference/endpoints/app-connections/supabase/create",
|
||||
"api-reference/endpoints/app-connections/supabase/update",
|
||||
"api-reference/endpoints/app-connections/supabase/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "TeamCity",
|
||||
"pages": [
|
||||
@ -1708,6 +1720,19 @@
|
||||
"api-reference/endpoints/secret-syncs/camunda/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Checkly",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/checkly/list",
|
||||
"api-reference/endpoints/secret-syncs/checkly/get-by-id",
|
||||
"api-reference/endpoints/secret-syncs/checkly/get-by-name",
|
||||
"api-reference/endpoints/secret-syncs/checkly/create",
|
||||
"api-reference/endpoints/secret-syncs/checkly/update",
|
||||
"api-reference/endpoints/secret-syncs/checkly/delete",
|
||||
"api-reference/endpoints/secret-syncs/checkly/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/checkly/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloudflare Pages",
|
||||
"pages": [
|
||||
@ -1882,6 +1907,19 @@
|
||||
"api-reference/endpoints/secret-syncs/render/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Supabase",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/supabase/list",
|
||||
"api-reference/endpoints/secret-syncs/supabase/get-by-id",
|
||||
"api-reference/endpoints/secret-syncs/supabase/get-by-name",
|
||||
"api-reference/endpoints/secret-syncs/supabase/create",
|
||||
"api-reference/endpoints/secret-syncs/supabase/update",
|
||||
"api-reference/endpoints/secret-syncs/supabase/delete",
|
||||
"api-reference/endpoints/secret-syncs/supabase/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/supabase/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "TeamCity",
|
||||
"pages": [
|
||||
|
@ -42,10 +42,14 @@ To be more specific:
|
||||
Most of the time, the Infisical server will be behind a load balancer or
|
||||
proxy. To propagate the TLS certificate from the load balancer to the
|
||||
instance, you can configure the TLS to send the client certificate as a header
|
||||
that is set as an [environment
|
||||
variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
|
||||
that is set as an [environment variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
|
||||
</Accordion>
|
||||
|
||||
<Note>
|
||||
Infisical US/EU and dedicated instances are deployed with AWS ALB. TLS Certificate Auth must flow through our ALB mTLS pass-through in order to authenticate.
|
||||
When you are authenticating with TLS Certificate Auth, you must use the port `8443` instead of the default `443`. Example: `https://app.infisical.com:8443/api/v1/auth/tls-cert-auth/login`
|
||||
</Note>
|
||||
|
||||
## Guide
|
||||
|
||||
In the following steps, we explore how to create and use identities for your workloads and applications on TLS Certificate to
|
||||
@ -123,7 +127,7 @@ try {
|
||||
const clientCertificate = fs.readFileSync("client-cert.pem", "utf8");
|
||||
const clientKeyCertificate = fs.readFileSync("client-key.pem", "utf8");
|
||||
|
||||
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
|
||||
const infisicalUrl = "https://app.infisical.com:8443"; // or your self-hosted Infisical URL
|
||||
const identityId = "<your-identity-id>";
|
||||
|
||||
// Create HTTPS agent with client certificate and key
|
||||
|
After Width: | Height: | Size: 532 KiB |
After Width: | Height: | Size: 434 KiB |
After Width: | Height: | Size: 466 KiB |
After Width: | Height: | Size: 491 KiB |
After Width: | Height: | Size: 702 KiB |
After Width: | Height: | Size: 505 KiB |
After Width: | Height: | Size: 571 KiB |
After Width: | Height: | Size: 336 KiB |
After Width: | Height: | Size: 428 KiB |
BIN
docs/images/app-connections/railway/SCR-20250712-pjrc.png
Normal file
After Width: | Height: | Size: 690 KiB |
BIN
docs/images/app-connections/supabase/app-connection-api-keys.png
Normal file
After Width: | Height: | Size: 251 KiB |
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 275 KiB |
BIN
docs/images/app-connections/supabase/app-connection-form.png
Normal file
After Width: | Height: | Size: 494 KiB |
After Width: | Height: | Size: 684 KiB |
After Width: | Height: | Size: 253 KiB |
BIN
docs/images/app-connections/supabase/app-connection-option.png
Normal file
After Width: | Height: | Size: 413 KiB |
After Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 865 KiB After Width: | Height: | Size: 894 KiB |
Before Width: | Height: | Size: 652 KiB After Width: | Height: | Size: 666 KiB |