mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 13:22:08 +00:00
--------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Scott Wilson <[email protected]>
This commit is contained in:
co-authored by
greptile-apps[bot]
Scott Wilson
parent
0e07ebae7b
commit
ecc15bb432
@@ -31,7 +31,8 @@ export enum AppConnection {
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
Bitbucket = "bitbucket",
|
||||
Checkly = "checkly"
|
||||
Checkly = "checkly",
|
||||
Supabase = "supabase"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -95,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,
|
||||
@@ -148,7 +153,8 @@ export const listAppConnectionOptions = () => {
|
||||
getZabbixConnectionListItem(),
|
||||
getRailwayConnectionListItem(),
|
||||
getBitbucketConnectionListItem(),
|
||||
getChecklyConnectionListItem()
|
||||
getChecklyConnectionListItem(),
|
||||
getSupabaseConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -232,7 +238,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -292,6 +299,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
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}`);
|
||||
@@ -355,7 +364,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Checkly]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Supabase]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
||||
@@ -33,7 +33,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Zabbix]: "Zabbix",
|
||||
[AppConnection.Railway]: "Railway",
|
||||
[AppConnection.Bitbucket]: "Bitbucket",
|
||||
[AppConnection.Checkly]: "Checkly"
|
||||
[AppConnection.Checkly]: "Checkly",
|
||||
[AppConnection.Supabase]: "Supabase"
|
||||
};
|
||||
|
||||
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -69,5 +70,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Checkly]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Supabase]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -78,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";
|
||||
@@ -131,7 +133,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
||||
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema
|
||||
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
|
||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -545,6 +548,7 @@ export const appConnectionServiceFactory = ({
|
||||
zabbix: zabbixConnectionService(connectAppConnectionById),
|
||||
railway: railwayConnectionService(connectAppConnectionById),
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||
checkly: checklyConnectionService(connectAppConnectionById)
|
||||
checkly: checklyConnectionService(connectAppConnectionById),
|
||||
supabase: supabaseConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -159,6 +159,12 @@ import {
|
||||
TRenderConnectionInput,
|
||||
TValidateRenderConnectionCredentialsSchema
|
||||
} from "./render/render-connection-types";
|
||||
import {
|
||||
TSupabaseConnection,
|
||||
TSupabaseConnectionConfig,
|
||||
TSupabaseConnectionInput,
|
||||
TValidateSupabaseConnectionCredentialsSchema
|
||||
} from "./supabase";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
@@ -224,6 +230,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
| TSupabaseConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -264,6 +271,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TZabbixConnectionInput
|
||||
| TRailwayConnectionInput
|
||||
| TChecklyConnectionInput
|
||||
| TSupabaseConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -311,7 +319,8 @@ export type TAppConnectionConfig =
|
||||
| TBitbucketConnectionConfig
|
||||
| TZabbixConnectionConfig
|
||||
| TRailwayConnectionConfig
|
||||
| TChecklyConnectionConfig;
|
||||
| TChecklyConnectionConfig
|
||||
| TSupabaseConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -346,7 +355,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateBitbucketConnectionCredentialsSchema
|
||||
| TValidateZabbixConnectionCredentialsSchema
|
||||
| TValidateRailwayConnectionCredentialsSchema
|
||||
| TValidateChecklyConnectionCredentialsSchema;
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
| TValidateSupabaseConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export enum SecretSync {
|
||||
GitLab = "gitlab",
|
||||
CloudflarePages = "cloudflare-pages",
|
||||
CloudflareWorkers = "cloudflare-workers",
|
||||
|
||||
Supabase = "supabase",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
Checkly = "checkly"
|
||||
|
||||
@@ -46,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";
|
||||
@@ -76,7 +77,7 @@ 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.Checkly]: CHECKLY_SYNC_LIST_OPTION
|
||||
@@ -255,6 +256,8 @@ export const SecretSyncFns = {
|
||||
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}`
|
||||
@@ -359,6 +362,9 @@ export const SecretSyncFns = {
|
||||
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}`
|
||||
@@ -444,6 +450,8 @@ export const SecretSyncFns = {
|
||||
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,7 +25,7 @@ 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.Checkly]: "Checkly"
|
||||
@@ -55,7 +55,7 @@ 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.Checkly]: AppConnection.Checkly
|
||||
@@ -85,7 +85,7 @@ 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.Checkly]: SecretSyncPlanType.Regular
|
||||
|
||||
@@ -118,6 +118,12 @@ import {
|
||||
TRenderSyncListItem,
|
||||
TRenderSyncWithCredentials
|
||||
} from "./render/render-sync-types";
|
||||
import {
|
||||
TSupabaseSync,
|
||||
TSupabaseSyncInput,
|
||||
TSupabaseSyncListItem,
|
||||
TSupabaseSyncWithCredentials
|
||||
} from "./supabase/supabase-sync-types";
|
||||
import {
|
||||
TTeamCitySync,
|
||||
TTeamCitySyncInput,
|
||||
@@ -159,7 +165,8 @@ export type TSecretSync =
|
||||
| TCloudflareWorkersSync
|
||||
| TZabbixSync
|
||||
| TRailwaySync
|
||||
| TChecklySync;
|
||||
| TChecklySync
|
||||
| TSupabaseSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -187,7 +194,8 @@ export type TSecretSyncWithCredentials =
|
||||
| TCloudflareWorkersSyncWithCredentials
|
||||
| TZabbixSyncWithCredentials
|
||||
| TRailwaySyncWithCredentials
|
||||
| TChecklySyncWithCredentials;
|
||||
| TChecklySyncWithCredentials
|
||||
| TSupabaseSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -215,7 +223,8 @@ export type TSecretSyncInput =
|
||||
| TCloudflareWorkersSyncInput
|
||||
| TZabbixSyncInput
|
||||
| TRailwaySyncInput
|
||||
| TChecklySyncInput;
|
||||
| TChecklySyncInput
|
||||
| TSupabaseSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -243,7 +252,8 @@ export type TSecretSyncListItem =
|
||||
| TCloudflareWorkersSyncListItem
|
||||
| TZabbixSyncListItem
|
||||
| TRailwaySyncListItem
|
||||
| TChecklySyncListItem;
|
||||
| TChecklySyncListItem
|
||||
| TSupabaseSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./supabase-sync-constants";
|
||||
export * from "./supabase-sync-fns";
|
||||
export * from "./supabase-sync-schemas";
|
||||
export * from "./supabase-sync-types";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const SUPABASE_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Supabase",
|
||||
destination: SecretSync.Supabase,
|
||||
connection: AppConnection.Supabase,
|
||||
canImportSecrets: false
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import { chunkArray } from "@app/lib/fn";
|
||||
import { TSupabaseSecret } from "@app/services/app-connection/supabase";
|
||||
import { SupabasePublicAPI } from "@app/services/app-connection/supabase/supabase-connection-public-client";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
|
||||
import { SecretSyncError } from "../secret-sync-errors";
|
||||
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
|
||||
import { TSecretMap } from "../secret-sync-types";
|
||||
import { TSupabaseSyncWithCredentials } from "./supabase-sync-types";
|
||||
|
||||
const SUPABASE_INTERNAL_SECRETS = ["SUPABASE_URL", "SUPABASE_ANON_KEY", "SUPABASE_SERVICE_ROLE_KEY", "SUPABASE_DB_URL"];
|
||||
|
||||
export const SupabaseSyncFns = {
|
||||
async getSecrets(secretSync: TSupabaseSyncWithCredentials) {
|
||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||
},
|
||||
|
||||
async syncSecrets(secretSync: TSupabaseSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const {
|
||||
environment,
|
||||
syncOptions: { disableSecretDeletion, keySchema }
|
||||
} = secretSync;
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const variables = await SupabasePublicAPI.getVariables(secretSync.connection, config.projectId);
|
||||
|
||||
const supabaseSecrets = new Map(variables!.map((variable) => [variable.name, variable]));
|
||||
|
||||
const toCreate: TSupabaseSecret[] = [];
|
||||
|
||||
for (const key of Object.keys(secretMap)) {
|
||||
const variable: TSupabaseSecret = { name: key, value: secretMap[key].value ?? "" };
|
||||
toCreate.push(variable);
|
||||
}
|
||||
|
||||
for await (const batch of chunkArray(toCreate, 100)) {
|
||||
try {
|
||||
await SupabasePublicAPI.createVariables(secretSync.connection, config.projectId, ...batch);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: batch[0].name // Use the first key in the batch for error reporting
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
const toDelete: string[] = [];
|
||||
|
||||
for (const key of supabaseSecrets.keys()) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema) || SUPABASE_INTERNAL_SECRETS.includes(key)) continue;
|
||||
|
||||
if (!secretMap[key]) {
|
||||
toDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for await (const batch of chunkArray(toDelete, 100)) {
|
||||
try {
|
||||
await SupabasePublicAPI.deleteVariables(secretSync.connection, config.projectId, ...batch);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: batch[0] // Use the first key in the batch for error reporting
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async removeSecrets(secretSync: TSupabaseSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const variables = await SupabasePublicAPI.getVariables(secretSync.connection, config.projectId);
|
||||
|
||||
const supabaseSecrets = new Map(variables!.map((variable) => [variable.name, variable]));
|
||||
|
||||
const toDelete: string[] = [];
|
||||
|
||||
for (const key of supabaseSecrets.keys()) {
|
||||
if (SUPABASE_INTERNAL_SECRETS.includes(key) || !(key in secretMap)) continue;
|
||||
|
||||
toDelete.push(key);
|
||||
}
|
||||
|
||||
for await (const batch of chunkArray(toDelete, 100)) {
|
||||
try {
|
||||
await SupabasePublicAPI.deleteVariables(secretSync.connection, config.projectId, ...batch);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: batch[0] // Use the first key in the batch for error reporting
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const SupabaseSyncDestinationConfigSchema = z.object({
|
||||
projectId: z.string().max(255).min(1, "Project ID is required"),
|
||||
projectName: z.string().max(255).min(1, "Project Name is required")
|
||||
});
|
||||
|
||||
const SupabaseSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||
|
||||
export const SupabaseSyncSchema = BaseSecretSyncSchema(SecretSync.Supabase, SupabaseSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Supabase),
|
||||
destinationConfig: SupabaseSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateSupabaseSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Supabase,
|
||||
SupabaseSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: SupabaseSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateSupabaseSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Supabase,
|
||||
SupabaseSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: SupabaseSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const SupabaseSyncListItemSchema = z.object({
|
||||
name: z.literal("Supabase"),
|
||||
connection: z.literal(AppConnection.Supabase),
|
||||
destination: z.literal(SecretSync.Supabase),
|
||||
canImportSecrets: z.literal(false)
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TSupabaseConnection } from "@app/services/app-connection/supabase";
|
||||
|
||||
import { CreateSupabaseSyncSchema, SupabaseSyncListItemSchema, SupabaseSyncSchema } from "./supabase-sync-schemas";
|
||||
|
||||
export type TSupabaseSyncListItem = z.infer<typeof SupabaseSyncListItemSchema>;
|
||||
|
||||
export type TSupabaseSync = z.infer<typeof SupabaseSyncSchema>;
|
||||
|
||||
export type TSupabaseSyncInput = z.infer<typeof CreateSupabaseSyncSchema>;
|
||||
|
||||
export type TSupabaseSyncWithCredentials = TSupabaseSync & {
|
||||
connection: TSupabaseConnection;
|
||||
};
|
||||
|
||||
export type TSupabaseVariablesGraphResponse = {
|
||||
data: {
|
||||
variables: Record<string, string>;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user