Merge branch 'main' into feat/secretsBatchUI

This commit is contained in:
Carlos Monastyrski
2025-07-18 15:40:07 -03:00
124 changed files with 2049 additions and 158 deletions
@@ -410,7 +410,7 @@ export const samlConfigServiceFactory = ({
}
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted);
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = crypto.jwt().sign(
{
+8
View File
@@ -2285,6 +2285,10 @@ export const AppConnections = {
},
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."
}
}
};
@@ -2494,6 +2498,10 @@ export const SecretSyncs = {
},
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."
}
}
};
+43
View File
@@ -0,0 +1,43 @@
import { FastifyReply } from "fastify";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
/**
* `aod` (Auth Origin Domain) cookie is used to store the origin domain of the application when user was last authenticated.
* This is useful for determining the target domain for authentication redirects, especially in cloud deployments.
* It is set only in cloud mode to ensure that the cookie is shared across subdomains.
*/
export function addAuthOriginDomainCookie(res: FastifyReply) {
try {
const appCfg = getConfig();
// Only set the cookie if the app is running in cloud mode
if (!appCfg.isCloud) return;
const siteUrl = appCfg.SITE_URL!;
let domain: string;
const { hostname } = new URL(siteUrl);
const parts = hostname.split(".");
if (parts.length >= 2) {
// For `app.infisical.com` => `.infisical.com`
domain = `.${parts.slice(-2).join(".")}`;
} else {
// If somehow only "example", fallback to itself
domain = `.${hostname}`;
}
void res.setCookie("aod", siteUrl, {
domain,
path: "/",
sameSite: "strict",
httpOnly: false,
secure: appCfg.HTTPS_ENABLED
});
} catch (error) {
logger.error(error, "Failed to set auth origin domain cookie");
}
}
@@ -12,6 +12,7 @@ import { getConfig, overridableKeys } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -593,6 +594,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
message: "Successfully set up admin account",
user: user.user,
@@ -83,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
@@ -133,7 +137,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedBitbucketConnectionSchema.options,
...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options
...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -169,7 +174,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
BitbucketConnectionListItemSchema,
ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema
ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -28,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";
@@ -70,5 +71,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter,
[AppConnection.Checkly]: registerChecklyConnectionRouter
[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 };
}
});
};
@@ -42,6 +42,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
maxAge: 0
});
void res.cookie("aod", "", {
httpOnly: false,
path: "/",
sameSite: "lax",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return { message: "Successfully logged out" };
}
});
@@ -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)
}),
@@ -21,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";
@@ -53,7 +54,7 @@ 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.Checkly]: registerChecklySyncRouter
@@ -41,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";
@@ -71,7 +72,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
GitLabSyncSchema,
CloudflarePagesSyncSchema,
CloudflareWorkersSyncSchema,
SupabaseSyncSchema,
ZabbixSyncSchema,
RailwaySyncSchema,
ChecklySyncSchema
@@ -104,7 +105,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
ZabbixSyncListItemSchema,
RailwaySyncListItemSchema,
ChecklySyncListItemSchema
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
});
@@ -22,6 +22,7 @@ import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms";
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -475,6 +476,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,
@@ -4,6 +4,7 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { mfaRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
export const registerMfaRouter = async (server: FastifyZodProvider) => {
@@ -131,6 +132,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
...user,
token: token.access,
@@ -10,6 +10,7 @@ import {
import { ApiDocsTags, ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -396,6 +397,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return { organization, accessToken: tokens.accessToken };
}
});
@@ -3,6 +3,7 @@ import { z } from "zod";
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
export const registerLoginRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -93,6 +94,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
@@ -155,6 +158,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
@@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors";
import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -170,6 +171,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return { message: "Successfully set up account", user, token: accessToken, organizationId };
}
});
@@ -239,6 +242,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
});
// TODO(akhilmhdh-pg): add telemetry service
addAuthOriginDomainCookie(res);
return { message: "Successfully set up account", user, token: accessToken };
}
});
@@ -31,12 +31,16 @@ export const validateOnePassConnectionCredentials = async (config: TOnePassConne
const { apiToken } = config.credentials;
try {
await request.get(`${instanceUrl}/v1/vaults`, {
const res = await request.get(`${instanceUrl}/v1/vaults`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
if (!Array.isArray(res.data)) {
throw new AxiosError("Invalid response from 1Password API");
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
@@ -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;
@@ -9,6 +9,7 @@ import { getAppConnectionMethodName } from "@app/services/app-connection/app-con
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { GithubTokenRespData, isGithubErrorResponse } from "../github/github-connection-fns";
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
import {
TGitHubRadarConnection,
@@ -71,13 +72,6 @@ export const listGitHubRadarRepositories = async (appConnection: TGitHubRadarCon
return repositories;
};
type TokenRespData = {
access_token: string;
scope: string;
token_type: string;
error?: string;
};
export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRadarConnectionConfig) => {
const { credentials, method } = config;
@@ -93,10 +87,10 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
});
}
let tokenResp: AxiosResponse<TokenRespData>;
let tokenResp: AxiosResponse<GithubTokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID,
client_secret: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET,
@@ -108,19 +102,27 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
"Accept-Encoding": "application/json"
}
});
if (isGithubErrorResponse(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`
});
}
if (tokenResp.status !== 200) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
});
}
if (method === GitHubRadarConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{
installations: {
id: number;
@@ -149,10 +151,6 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubRadarConnectionMethod.App:
return {
@@ -144,13 +144,21 @@ export const getGitHubEnvironments = async (appConnection: TGitHubConnection, ow
}
};
type TokenRespData = {
access_token: string;
export type GithubTokenRespData = {
access_token?: string;
scope: string;
token_type: string;
error?: string;
};
export function isGithubErrorResponse(data: GithubTokenRespData): data is GithubTokenRespData & {
error: string;
error_description: string;
error_uri: string;
} {
return "error" in data;
}
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config;
@@ -183,10 +191,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
});
}
let tokenResp: AxiosResponse<TokenRespData>;
let tokenResp: AxiosResponse<GithubTokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: clientId,
client_secret: clientSecret,
@@ -198,7 +206,17 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
"Accept-Encoding": "application/json"
}
});
if (isGithubErrorResponse(tokenResp?.data)) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
});
}
} catch (e: unknown) {
if (e instanceof BadRequestError) {
throw e;
}
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
@@ -211,6 +229,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
if (method === GitHubConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{
installations: {
id: number;
@@ -239,10 +261,6 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubConnectionMethod.App:
return {
@@ -0,0 +1,4 @@
export * from "./supabase-connection-constants";
export * from "./supabase-connection-fns";
export * from "./supabase-connection-schemas";
export * from "./supabase-connection-types";
@@ -0,0 +1,3 @@
export enum SupabaseConnectionMethod {
AccessToken = "access-token"
}
@@ -0,0 +1,58 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
import { SupabasePublicAPI } from "./supabase-connection-public-client";
import { TSupabaseConnection, TSupabaseConnectionConfig } from "./supabase-connection-types";
export const getSupabaseConnectionListItem = () => {
return {
name: "Supabase" as const,
app: AppConnection.Supabase as const,
methods: Object.values(SupabaseConnectionMethod)
};
};
export const validateSupabaseConnectionCredentials = async (config: TSupabaseConnectionConfig) => {
const { credentials } = config;
try {
await SupabasePublicAPI.healthcheck(config);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection - verify credentials"
});
}
return credentials;
};
export const listProjects = async (appConnection: TSupabaseConnection) => {
try {
return await SupabasePublicAPI.getProjects(appConnection);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list projects: ${error.message || "Unknown error"}`
});
}
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: "Unable to list projects",
error
});
}
};
@@ -0,0 +1,133 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable class-methods-use-this */
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode } from "axios";
import { createRequestClient } from "@app/lib/config/request";
import { delay } from "@app/lib/delay";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
import { TSupabaseConnectionConfig, TSupabaseProject, TSupabaseSecret } from "./supabase-connection-types";
export const getSupabaseInstanceUrl = async (config: TSupabaseConnectionConfig) => {
const instanceUrl = config.credentials.instanceUrl
? removeTrailingSlash(config.credentials.instanceUrl)
: "https://api.supabase.com";
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export function getSupabaseAuthHeaders(connection: TSupabaseConnectionConfig): Record<string, string> {
switch (connection.method) {
case SupabaseConnectionMethod.AccessToken:
return {
Authorization: `Bearer ${connection.credentials.accessKey}`
};
default:
throw new Error(`Unsupported Supabase connection method`);
}
}
export function getSupabaseRatelimiter(response: AxiosResponse): {
maxAttempts: number;
isRatelimited: boolean;
wait: () => Promise<void>;
} {
const wait = () => {
return delay(60 * 1000);
};
return {
isRatelimited: response.status === HttpStatusCode.TooManyRequests,
wait,
maxAttempts: 3
};
}
class SupabasePublicClient {
private client: AxiosInstance;
constructor() {
this.client = createRequestClient({
headers: {
"Content-Type": "application/json"
}
});
}
async send<T>(
connection: TSupabaseConnectionConfig,
config: AxiosRequestConfig,
retryAttempt = 0
): Promise<T | undefined> {
const response = await this.client.request<T>({
...config,
baseURL: await getSupabaseInstanceUrl(connection),
validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests,
headers: getSupabaseAuthHeaders(connection)
});
const limiter = getSupabaseRatelimiter(response);
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
await limiter.wait();
return this.send(connection, config, retryAttempt + 1);
}
return response.data;
}
async healthcheck(connection: TSupabaseConnectionConfig) {
switch (connection.method) {
case SupabaseConnectionMethod.AccessToken:
return void (await this.getProjects(connection));
default:
throw new Error(`Unsupported Supabase connection method`);
}
}
async getVariables(connection: TSupabaseConnectionConfig, projectRef: string) {
const res = await this.send<TSupabaseSecret[]>(connection, {
method: "GET",
url: `/v1/projects/${projectRef}/secrets`
});
return res;
}
// Supabase does not support updating variables directly
// Instead, just call create again with the same key and it will overwrite the existing variable
async createVariables(connection: TSupabaseConnectionConfig, projectRef: string, ...variables: TSupabaseSecret[]) {
const res = await this.send<TSupabaseSecret>(connection, {
method: "POST",
url: `/v1/projects/${projectRef}/secrets`,
data: variables
});
return res;
}
async deleteVariables(connection: TSupabaseConnectionConfig, projectRef: string, ...variables: string[]) {
const res = await this.send(connection, {
method: "DELETE",
url: `/v1/projects/${projectRef}/secrets`,
data: variables
});
return res;
}
async getProjects(connection: TSupabaseConnectionConfig) {
const res = await this.send<TSupabaseProject[]>(connection, {
method: "GET",
url: `/v1/projects`
});
return res;
}
}
export const SupabasePublicAPI = new SupabasePublicClient();
@@ -0,0 +1,70 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
export const SupabaseConnectionMethodSchema = z
.nativeEnum(SupabaseConnectionMethod)
.describe(AppConnections.CREATE(AppConnection.Supabase).method);
export const SupabaseConnectionAccessTokenCredentialsSchema = z.object({
accessKey: z
.string()
.trim()
.min(1, "Access Key required")
.max(255)
.describe(AppConnections.CREDENTIALS.SUPABASE.accessKey),
instanceUrl: z.string().trim().url().max(255).describe(AppConnections.CREDENTIALS.SUPABASE.instanceUrl).optional()
});
const BaseSupabaseConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Supabase)
});
export const SupabaseConnectionSchema = BaseSupabaseConnectionSchema.extend({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema
});
export const SanitizedSupabaseConnectionSchema = z.discriminatedUnion("method", [
BaseSupabaseConnectionSchema.extend({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateSupabaseConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Supabase).credentials
)
})
]);
export const CreateSupabaseConnectionSchema = ValidateSupabaseConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Supabase)
);
export const UpdateSupabaseConnectionSchema = z
.object({
credentials: SupabaseConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Supabase).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Supabase));
export const SupabaseConnectionListItemSchema = z.object({
name: z.literal("Supabase"),
app: z.literal(AppConnection.Supabase),
methods: z.nativeEnum(SupabaseConnectionMethod).array()
});
@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listProjects as getSupabaseProjects } from "./supabase-connection-fns";
import { TSupabaseConnection } from "./supabase-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TSupabaseConnection>;
export const supabaseConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Supabase, connectionId, actor);
try {
const projects = await getSupabaseProjects(appConnection);
return projects ?? [];
} catch (error) {
logger.error(error, "Failed to establish connection with Supabase");
return [];
}
};
return {
listProjects
};
};
@@ -0,0 +1,44 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateSupabaseConnectionSchema,
SupabaseConnectionSchema,
ValidateSupabaseConnectionCredentialsSchema
} from "./supabase-connection-schemas";
export type TSupabaseConnection = z.infer<typeof SupabaseConnectionSchema>;
export type TSupabaseConnectionInput = z.infer<typeof CreateSupabaseConnectionSchema> & {
app: AppConnection.Supabase;
};
export type TValidateSupabaseConnectionCredentialsSchema = typeof ValidateSupabaseConnectionCredentialsSchema;
export type TSupabaseConnectionConfig = DiscriminativePick<TSupabaseConnection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TSupabaseProject = {
id: string;
organization_id: string;
name: string;
region: string;
created_at: Date;
status: string;
database: TSupabaseDatabase;
};
type TSupabaseDatabase = {
host: string;
version: string;
postgres_engine: string;
release_channel: string;
};
export type TSupabaseSecret = {
name: string;
value: string;
};
@@ -218,7 +218,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
};
const findWithAssociatedCa = async (
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string },
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string },
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
tx?: Knex
) => {
@@ -1068,11 +1068,11 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate chain" });
const parentCertObj = chainItems[1];
const parentCertSubject = parentCertObj.subject;
const parentSerialNumber = parentCertObj.serialNumber;
const [parentCa] = await certificateAuthorityDAL.findWithAssociatedCa({
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: ca.projectId,
[`${TableName.InternalCertificateAuthority}.dn` as "dn"]: parentCertSubject
[`${TableName.InternalCertificateAuthority}.serialNumber` as "serialNumber"]: parentSerialNumber
});
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
@@ -6,7 +6,8 @@ export type TLoginOciAuthDTO = {
headers: {
authorization: string;
host: string;
"x-date": string;
"x-date"?: string;
date?: string;
};
};
@@ -174,6 +174,7 @@ export const fnSecretsV2FromImports = async ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => Promise<string | undefined>;
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => {
@@ -293,7 +294,8 @@ export const fnSecretsV2FromImports = async ({
value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath,
environment: processedImport.environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";
@@ -1,4 +1,6 @@
/* eslint-disable no-await-in-loop */
import { isAxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
@@ -71,7 +73,7 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr
);
};
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: TRenderSecret) => {
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick<TRenderSecret, "key">) => {
const {
destinationConfig,
connection: {
@@ -79,15 +81,24 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s
}
} = secretSync;
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
try {
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
);
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
// If the secret does not exist, we can ignore this error
return;
}
);
throw error;
}
};
const sleep = async () =>
@@ -99,6 +110,11 @@ export const RenderSyncFns = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
for await (const key of Object.keys(secretMap)) {
// If value is empty skip it as render does not allow empty variables
if (secretMap[key].value === "") {
// eslint-disable-next-line no-continue
continue;
}
await putEnvironmentSecret(secretSync, secretMap, key);
await sleep();
}
@@ -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
@@ -231,7 +231,8 @@ export const secretSyncQueueFactory = ({
environment: environment.slug,
secretPath: folder.path,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
secretMap[secretKey] = { value: expandedSecretValue || "" };
@@ -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>;
};
};
@@ -629,6 +629,7 @@ export const expandSecretReferencesFactory = ({
secretPath: string;
environment: string;
shouldStackTrace?: boolean;
secretKey: string;
}) => {
const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode;
@@ -671,7 +672,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretKey}' in environment '${environment}' at path '${secretPath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(environment, secretPath);
@@ -690,7 +691,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretReferenceKey}' in environment '${secretReferenceEnvironment}' at path '${secretReferencePath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@@ -707,6 +708,7 @@ export const expandSecretReferencesFactory = ({
secretPath: referencedSecretPath,
environment: referencedSecretEnvironmentSlug,
depth: depth + 1,
secretKey: referencedSecretKey,
trace
};
@@ -741,6 +743,7 @@ export const expandSecretReferencesFactory = ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
@@ -756,6 +759,7 @@ export const expandSecretReferencesFactory = ({
value?: string;
secretPath: string;
environment: string;
secretKey: string;
}) => {
const { stackTrace, expandedValue } = await recursivelyExpandSecret({ ...inputSecret, shouldStackTrace: true });
return { stackTrace, expandedValue };
@@ -1105,7 +1105,7 @@ export const secretV2BridgeServiceFactory = ({
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
await Promise.allSettled(
const settledPromises = await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
@@ -1113,7 +1113,8 @@ export const secretV2BridgeServiceFactory = ({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
@@ -1121,6 +1122,35 @@ export const secretV2BridgeServiceFactory = ({
)
)
);
const errors: { path: string; error: string }[] = [];
settledPromises.forEach((outerResult: PromiseSettledResult<PromiseSettledResult<void>[]>, outerIndex) => {
const groupedPath = Object.keys(secretsGroupByPath)[outerIndex];
if (outerResult.status === "rejected") {
errors.push({
path: groupedPath,
error: `Failed to process secret group: ${outerResult.reason}`
});
} else {
// Check inner promise results
outerResult.value.forEach((innerResult: PromiseSettledResult<void>) => {
if (innerResult.status === "rejected") {
const reason = innerResult.reason as ForbiddenRequestError;
errors.push({
path: groupedPath,
error: reason.message
});
}
});
}
});
if (errors.length > 0) {
throw new ForbiddenRequestError({
message: "Failed to expand one or more secret references",
details: errors.map((err) => err.error)
});
}
}
if (!includeImports) {
@@ -1424,7 +1454,8 @@ export const secretV2BridgeServiceFactory = ({
environment,
secretPath: path,
value: secretValue,
skipMultilineEncoding: secret.skipMultilineEncoding
skipMultilineEncoding: secret.skipMultilineEncoding,
secretKey: secret.key
});
secretValue = expandedSecretValue || "";
@@ -2748,7 +2779,8 @@ export const secretV2BridgeServiceFactory = ({
const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({
environment,
secretPath,
value: decryptedSecretValue
value: decryptedSecretValue,
secretKey: secretName
});
return { tree: stackTrace, value: expandedValue };
+2 -1
View File
@@ -426,7 +426,8 @@ export const secretQueueFactory = ({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
content[secretKey] = { value: expandedSecretValue || "" };