Compare commits

...

29 Commits

Author SHA1 Message Date
350afee45e Updated cli export doc 2025-07-17 10:00:40 -03:00
8187b1da91 Updated CLI export doc to document the new --output-file behavior 2025-07-17 06:58:34 -03:00
fd761df8e5 Merge pull request #4178 from Infisical/access-request-env-view
improvement(access-requests): add access requests to single env view + general UI improvements
2025-07-16 16:25:44 -07:00
61ca617616 improvement: address feedback 2025-07-16 16:20:10 -07:00
6ce6c276cd Merge pull request #4180 from Infisical/daniel/tls-auth-docs
docs: document use of port 8433 for TLS certificate auth
2025-07-17 00:45:08 +04:00
32b2f7b0fe fix typo 2025-07-17 00:20:02 +04:00
4c2823c480 Update login.mdx 2025-07-17 00:09:56 +04:00
60438694e4 Update tls-cert-auth.mdx 2025-07-17 00:08:34 +04:00
fdaf8f9a87 Merge pull request #4179 from Infisical/doc/added-section-about-sales-approval-design-doc
doc: added section about sales approval
2025-07-16 16:07:36 -04:00
3fe41f81fe improvement: address feedback 2025-07-16 12:52:05 -07:00
Sid
c1798d37be fix: propogate Github app connection errors to the client properly (#4177)
* fix: propogate github errors to the client properly
2025-07-17 01:14:06 +05:30
01c6d3192d doc: added section about sales approval 2025-07-17 03:31:58 +08:00
621bfe3e60 chore: revert license 2025-07-16 12:17:43 -07:00
67ec00d46b feature: add access requests to single env view, with general UI improvements 2025-07-16 12:16:13 -07:00
d6c2789d46 Merge pull request #4176 from Infisical/ENG-3154
Make certificate collection required
2025-07-16 14:29:42 -04:00
58ba0c8ed4 Merge pull request #4175 from Infisical/fix/samlNotVerifiedEmailFix
Add isEmailVerified to isUserCompleted flag on samlLogin
2025-07-16 15:23:52 -03:00
f38c574030 Address review 2025-07-16 14:01:55 -04:00
c330d8ca8a Make certificate collection required 2025-07-16 13:53:52 -04:00
2cb0ecc768 Add isEmailVerified to isUserCompleted flag on samlLogin 2025-07-16 14:20:37 -03:00
Sid
ecc15bb432 feat(#2938): Add supabase app connection and secrets sync (#4113)
---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Scott Wilson <scottraywilson@gmail.com>
2025-07-16 22:06:11 +05:30
0e07ebae7b fix: oci auth for go sdk (#4152) 2025-07-16 16:36:28 +05:30
a94a26263a Merge pull request #4115 from Infisical/fix/postgresAppConnectionDocTip
Minor improvement on the Postgres docs changing a warning to a tip
2025-07-15 21:47:42 -03:00
b4ef55db4e Minor improvement on the Postgres docs changing a warning to a tip 2025-07-15 21:45:31 -03:00
307b5d1f87 Merge pull request #4112 from Infisical/misc/re-added-est
misc: re-added EST to PKI templates
2025-07-15 17:00:24 -07:00
54087038c2 Merge pull request #4106 from Infisical/secret-change-status-badge
improvement(frontend): add merge/closed status badge to closed secret change request table
2025-07-15 14:03:23 -07:00
f835bf0ba8 Merge pull request #4111 from Infisical/fix/improvePostgresDocs
Add missing setting for postgres app connection
2025-07-15 16:58:13 -03:00
c79ea0631e misc: re-added EST 2025-07-16 03:12:49 +08:00
c4e08b9811 improvement: change closed to rejected and address feedback 2025-07-14 19:15:52 -07:00
7784b8a81c improvement: add merge/closed status badge to closed secret change request table 2025-07-14 19:10:28 -07:00
112 changed files with 1970 additions and 144 deletions

View File

@ -410,7 +410,7 @@ export const samlConfigServiceFactory = ({
} }
await licenseService.updateSubscriptionOrgMemberCount(organization.id); 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 userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = crypto.jwt().sign( const providerAuthToken = crypto.jwt().sign(
{ {

View File

@ -2285,6 +2285,10 @@ export const AppConnections = {
}, },
CHECKLY: { CHECKLY: {
apiKey: "The API key used to authenticate with 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: { CHECKLY: {
accountId: "The ID of the Checkly account to sync secrets to." 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."
} }
} }
}; };

View File

@ -83,6 +83,10 @@ import {
RenderConnectionListItemSchema, RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema SanitizedRenderConnectionSchema
} from "@app/services/app-connection/render/render-connection-schema"; } from "@app/services/app-connection/render/render-connection-schema";
import {
SanitizedSupabaseConnectionSchema,
SupabaseConnectionListItemSchema
} from "@app/services/app-connection/supabase";
import { import {
SanitizedTeamCityConnectionSchema, SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema TeamCityConnectionListItemSchema
@ -133,7 +137,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedBitbucketConnectionSchema.options, ...SanitizedBitbucketConnectionSchema.options,
...SanitizedZabbixConnectionSchema.options, ...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options, ...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options ...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -169,7 +174,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
BitbucketConnectionListItemSchema, BitbucketConnectionListItemSchema,
ZabbixConnectionListItemSchema, ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema, RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@ -28,6 +28,7 @@ import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router"; import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router"; import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-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.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter, [AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter, [AppConnection.Railway]: registerRailwayConnectionRouter,
[AppConnection.Checkly]: registerChecklyConnectionRouter [AppConnection.Checkly]: registerChecklyConnectionRouter,
[AppConnection.Supabase]: registerSupabaseConnectionRouter
}; };

View File

@ -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 };
}
});
};

View File

@ -28,7 +28,17 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
.object({ .object({
authorization: z.string(), authorization: z.string(),
host: 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) .describe(OCI_AUTH.LOGIN.headers)
}), }),

View File

@ -21,6 +21,7 @@ import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router"; import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router"; import { registerRenderSyncRouter } from "./render-sync-router";
import { registerSupabaseSyncRouter } from "./supabase-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-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.GitLab]: registerGitLabSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter, [SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter,
[SecretSync.CloudflareWorkers]: registerCloudflareWorkersSyncRouter, [SecretSync.CloudflareWorkers]: registerCloudflareWorkersSyncRouter,
[SecretSync.Supabase]: registerSupabaseSyncRouter,
[SecretSync.Zabbix]: registerZabbixSyncRouter, [SecretSync.Zabbix]: registerZabbixSyncRouter,
[SecretSync.Railway]: registerRailwaySyncRouter, [SecretSync.Railway]: registerRailwaySyncRouter,
[SecretSync.Checkly]: registerChecklySyncRouter [SecretSync.Checkly]: registerChecklySyncRouter

View File

@ -41,6 +41,7 @@ import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas"; 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 { 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 { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@ -71,7 +72,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
GitLabSyncSchema, GitLabSyncSchema,
CloudflarePagesSyncSchema, CloudflarePagesSyncSchema,
CloudflareWorkersSyncSchema, CloudflareWorkersSyncSchema,
SupabaseSyncSchema,
ZabbixSyncSchema, ZabbixSyncSchema,
RailwaySyncSchema, RailwaySyncSchema,
ChecklySyncSchema ChecklySyncSchema
@ -104,7 +105,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
ZabbixSyncListItemSchema, ZabbixSyncListItemSchema,
RailwaySyncListItemSchema, RailwaySyncListItemSchema,
ChecklySyncListItemSchema ChecklySyncListItemSchema,
SupabaseSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@ -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
});

View File

@ -31,7 +31,8 @@ export enum AppConnection {
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",
Bitbucket = "bitbucket", Bitbucket = "bitbucket",
Checkly = "checkly" Checkly = "checkly",
Supabase = "supabase"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@ -95,6 +95,11 @@ import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postg
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { RenderConnectionMethod } from "./render/render-connection-enums"; import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns"; import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import {
getSupabaseConnectionListItem,
SupabaseConnectionMethod,
validateSupabaseConnectionCredentials
} from "./supabase";
import { import {
getTeamCityConnectionListItem, getTeamCityConnectionListItem,
TeamCityConnectionMethod, TeamCityConnectionMethod,
@ -148,7 +153,8 @@ export const listAppConnectionOptions = () => {
getZabbixConnectionListItem(), getZabbixConnectionListItem(),
getRailwayConnectionListItem(), getRailwayConnectionListItem(),
getBitbucketConnectionListItem(), getBitbucketConnectionListItem(),
getChecklyConnectionListItem() getChecklyConnectionListItem(),
getSupabaseConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@ -232,7 +238,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials 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); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@ -292,6 +299,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case RenderConnectionMethod.ApiKey: case RenderConnectionMethod.ApiKey:
case ChecklyConnectionMethod.ApiKey: case ChecklyConnectionMethod.ApiKey:
return "API Key"; return "API Key";
case SupabaseConnectionMethod.AccessToken:
return "Access Token";
default: default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`); throw new Error(`Unhandled App Connection Method: ${method}`);
@ -355,7 +364,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported, [AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
[AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Checkly]: platformManagedCredentialsNotSupported [AppConnection.Checkly]: platformManagedCredentialsNotSupported,
[AppConnection.Supabase]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (

View File

@ -33,7 +33,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Zabbix]: "Zabbix", [AppConnection.Zabbix]: "Zabbix",
[AppConnection.Railway]: "Railway", [AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Checkly]: "Checkly" [AppConnection.Checkly]: "Checkly",
[AppConnection.Supabase]: "Supabase"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { 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.Zabbix]: AppConnectionPlanType.Regular,
[AppConnection.Railway]: AppConnectionPlanType.Regular, [AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular, [AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Checkly]: AppConnectionPlanType.Regular [AppConnection.Checkly]: AppConnectionPlanType.Regular,
[AppConnection.Supabase]: AppConnectionPlanType.Regular
}; };

View File

@ -78,6 +78,8 @@ import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service"; import { railwayConnectionService } from "./railway/railway-connection-service";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema"; import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service"; import { renderConnectionService } from "./render/render-connection-service";
import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase";
import { supabaseConnectionService } from "./supabase/supabase-connection-service";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
@ -131,7 +133,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema, [AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema, [AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema, [AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema [AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@ -545,6 +548,7 @@ export const appConnectionServiceFactory = ({
zabbix: zabbixConnectionService(connectAppConnectionById), zabbix: zabbixConnectionService(connectAppConnectionById),
railway: railwayConnectionService(connectAppConnectionById), railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById), bitbucket: bitbucketConnectionService(connectAppConnectionById),
checkly: checklyConnectionService(connectAppConnectionById) checkly: checklyConnectionService(connectAppConnectionById),
supabase: supabaseConnectionService(connectAppConnectionById)
}; };
}; };

View File

@ -159,6 +159,12 @@ import {
TRenderConnectionInput, TRenderConnectionInput,
TValidateRenderConnectionCredentialsSchema TValidateRenderConnectionCredentialsSchema
} from "./render/render-connection-types"; } from "./render/render-connection-types";
import {
TSupabaseConnection,
TSupabaseConnectionConfig,
TSupabaseConnectionInput,
TValidateSupabaseConnectionCredentialsSchema
} from "./supabase";
import { import {
TTeamCityConnection, TTeamCityConnection,
TTeamCityConnectionConfig, TTeamCityConnectionConfig,
@ -224,6 +230,7 @@ export type TAppConnection = { id: string } & (
| TZabbixConnection | TZabbixConnection
| TRailwayConnection | TRailwayConnection
| TChecklyConnection | TChecklyConnection
| TSupabaseConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@ -264,6 +271,7 @@ export type TAppConnectionInput = { id: string } & (
| TZabbixConnectionInput | TZabbixConnectionInput
| TRailwayConnectionInput | TRailwayConnectionInput
| TChecklyConnectionInput | TChecklyConnectionInput
| TSupabaseConnectionInput
); );
export type TSqlConnectionInput = export type TSqlConnectionInput =
@ -311,7 +319,8 @@ export type TAppConnectionConfig =
| TBitbucketConnectionConfig | TBitbucketConnectionConfig
| TZabbixConnectionConfig | TZabbixConnectionConfig
| TRailwayConnectionConfig | TRailwayConnectionConfig
| TChecklyConnectionConfig; | TChecklyConnectionConfig
| TSupabaseConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@ -346,7 +355,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateBitbucketConnectionCredentialsSchema | TValidateBitbucketConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema | TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema | TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema; | TValidateChecklyConnectionCredentialsSchema
| TValidateSupabaseConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@ -145,12 +145,20 @@ export const getGitHubEnvironments = async (appConnection: TGitHubConnection, ow
}; };
type TokenRespData = { type TokenRespData = {
access_token: string; access_token?: string;
scope: string; scope: string;
token_type: string; token_type: string;
error?: 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) => { export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config; const { credentials, method } = config;
@ -198,7 +206,17 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
"Accept-Encoding": "application/json" "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) { } catch (e: unknown) {
if (e instanceof BadRequestError) {
throw e;
}
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: `Unable to validate connection: verify credentials`
}); });
@ -211,6 +229,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
} }
if (method === GitHubConnectionMethod.App) { if (method === GitHubConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{ const installationsResp = await request.get<{
installations: { installations: {
id: number; 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) { switch (method) {
case GitHubConnectionMethod.App: case GitHubConnectionMethod.App:
return { return {

View 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";

View File

@ -0,0 +1,3 @@
export enum SupabaseConnectionMethod {
AccessToken = "access-token"
}

View File

@ -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
});
}
};

View File

@ -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();

View File

@ -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()
});

View File

@ -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
};
};

View File

@ -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;
};

View File

@ -6,7 +6,8 @@ export type TLoginOciAuthDTO = {
headers: { headers: {
authorization: string; authorization: string;
host: string; host: string;
"x-date": string; "x-date"?: string;
date?: string;
}; };
}; };

View File

@ -22,7 +22,7 @@ export enum SecretSync {
GitLab = "gitlab", GitLab = "gitlab",
CloudflarePages = "cloudflare-pages", CloudflarePages = "cloudflare-pages",
CloudflareWorkers = "cloudflare-workers", CloudflareWorkers = "cloudflare-workers",
Supabase = "supabase",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",
Checkly = "checkly" Checkly = "checkly"

View File

@ -46,6 +46,7 @@ import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
import { RailwaySyncFns } from "./railway/railway-sync-fns"; import { RailwaySyncFns } from "./railway/railway-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render"; import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps"; 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 { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; 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.GitLab]: GITLAB_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION, [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION,
[SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, [SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION,
[SecretSync.Supabase]: SUPABASE_SYNC_LIST_OPTION,
[SecretSync.Zabbix]: ZABBIX_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 [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION
@ -255,6 +256,8 @@ export const SecretSyncFns = {
return RailwaySyncFns.syncSecrets(secretSync, schemaSecretMap); return RailwaySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Checkly: case SecretSync.Checkly:
return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap); return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@ -359,6 +362,9 @@ export const SecretSyncFns = {
case SecretSync.Checkly: case SecretSync.Checkly:
secretMap = await ChecklySyncFns.getSecrets(secretSync); secretMap = await ChecklySyncFns.getSecrets(secretSync);
break; break;
case SecretSync.Supabase:
secretMap = await SupabaseSyncFns.getSecrets(secretSync);
break;
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@ -444,6 +450,8 @@ export const SecretSyncFns = {
return RailwaySyncFns.removeSecrets(secretSync, schemaSecretMap); return RailwaySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Checkly: case SecretSync.Checkly:
return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap); return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@ -25,7 +25,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.GitLab]: "GitLab", [SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages", [SecretSync.CloudflarePages]: "Cloudflare Pages",
[SecretSync.CloudflareWorkers]: "Cloudflare Workers", [SecretSync.CloudflareWorkers]: "Cloudflare Workers",
[SecretSync.Supabase]: "Supabase",
[SecretSync.Zabbix]: "Zabbix", [SecretSync.Zabbix]: "Zabbix",
[SecretSync.Railway]: "Railway", [SecretSync.Railway]: "Railway",
[SecretSync.Checkly]: "Checkly" [SecretSync.Checkly]: "Checkly"
@ -55,7 +55,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.GitLab]: AppConnection.GitLab, [SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare, [SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Supabase]: AppConnection.Supabase,
[SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway, [SecretSync.Railway]: AppConnection.Railway,
[SecretSync.Checkly]: AppConnection.Checkly [SecretSync.Checkly]: AppConnection.Checkly
@ -85,7 +85,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.GitLab]: SecretSyncPlanType.Regular, [SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular, [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular,
[SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular, [SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular,
[SecretSync.Supabase]: SecretSyncPlanType.Regular,
[SecretSync.Zabbix]: SecretSyncPlanType.Regular, [SecretSync.Zabbix]: SecretSyncPlanType.Regular,
[SecretSync.Railway]: SecretSyncPlanType.Regular, [SecretSync.Railway]: SecretSyncPlanType.Regular,
[SecretSync.Checkly]: SecretSyncPlanType.Regular [SecretSync.Checkly]: SecretSyncPlanType.Regular

View File

@ -118,6 +118,12 @@ import {
TRenderSyncListItem, TRenderSyncListItem,
TRenderSyncWithCredentials TRenderSyncWithCredentials
} from "./render/render-sync-types"; } from "./render/render-sync-types";
import {
TSupabaseSync,
TSupabaseSyncInput,
TSupabaseSyncListItem,
TSupabaseSyncWithCredentials
} from "./supabase/supabase-sync-types";
import { import {
TTeamCitySync, TTeamCitySync,
TTeamCitySyncInput, TTeamCitySyncInput,
@ -159,7 +165,8 @@ export type TSecretSync =
| TCloudflareWorkersSync | TCloudflareWorkersSync
| TZabbixSync | TZabbixSync
| TRailwaySync | TRailwaySync
| TChecklySync; | TChecklySync
| TSupabaseSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials | TAwsParameterStoreSyncWithCredentials
@ -187,7 +194,8 @@ export type TSecretSyncWithCredentials =
| TCloudflareWorkersSyncWithCredentials | TCloudflareWorkersSyncWithCredentials
| TZabbixSyncWithCredentials | TZabbixSyncWithCredentials
| TRailwaySyncWithCredentials | TRailwaySyncWithCredentials
| TChecklySyncWithCredentials; | TChecklySyncWithCredentials
| TSupabaseSyncWithCredentials;
export type TSecretSyncInput = export type TSecretSyncInput =
| TAwsParameterStoreSyncInput | TAwsParameterStoreSyncInput
@ -215,7 +223,8 @@ export type TSecretSyncInput =
| TCloudflareWorkersSyncInput | TCloudflareWorkersSyncInput
| TZabbixSyncInput | TZabbixSyncInput
| TRailwaySyncInput | TRailwaySyncInput
| TChecklySyncInput; | TChecklySyncInput
| TSupabaseSyncInput;
export type TSecretSyncListItem = export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem | TAwsParameterStoreSyncListItem
@ -243,7 +252,8 @@ export type TSecretSyncListItem =
| TCloudflareWorkersSyncListItem | TCloudflareWorkersSyncListItem
| TZabbixSyncListItem | TZabbixSyncListItem
| TRailwaySyncListItem | TRailwaySyncListItem
| TChecklySyncListItem; | TChecklySyncListItem
| TSupabaseSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
canImportSecrets: boolean; canImportSecrets: boolean;

View 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";

View File

@ -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
};

View 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
});
}
}
}
};

View File

@ -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)
});

View File

@ -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>;
};
};

View File

@ -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). - A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs).
3. **Context** 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.). - 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** 4. **Solution**
- Provide a big-picture explanation of the solution, followed by detailed technical architecture. - 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. - 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** 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. - 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.

View File

@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/supabase/available"
---

View File

@ -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>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/supabase/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/supabase/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/supabase/connection-name/{connectionName}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/supabase"
---

View File

@ -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>

View File

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/supabase"
---

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/supabase/{syncId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/supabase/{syncId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/supabase/sync-name/{syncName}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/supabase"
---

View File

@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/supabase/{syncId}/remove-secrets"
---

View File

@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/supabase/{syncId}/sync-secrets"
---

View File

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/supabase/{syncId}"
---

View File

@ -2,3 +2,8 @@
title: "Login" title: "Login"
openapi: "POST /api/v1/auth/tls-cert-auth/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>

View File

@ -9,7 +9,7 @@ infisical export [options]
## Description ## 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 ## Subcommands & flags
@ -21,18 +21,19 @@ $ infisical export
# Export variables to a .env file # Export variables to a .env file
infisical export > .env infisical export > .env
infisical export --output-file=./.env
# Export variables to a .env file (with export keyword) # Export variables to a .env file (with export keyword)
infisical export --format=dotenv-export > .env infisical export --format=dotenv-export > .env
infisical export --format=dotenv-export --output-file=./.env
# Export variables to a CSV file
infisical export --format=csv > secrets.csv
# Export variables to a JSON file # Export variables to a JSON file
infisical export --format=json > secrets.json infisical export --format=json > secrets.json
infisical export --format=json --output-file=./secrets.json
# Export variables to a YAML file # Export variables to a YAML file
infisical export --format=yaml > secrets.yaml infisical export --format=yaml > secrets.yaml
infisical export --format=yaml --output-file=./secrets.yaml
# Render secrets using a custom template file # Render secrets using a custom template file
infisical export --template=<path to template> infisical export --template=<path to template>
@ -73,6 +74,34 @@ infisical export --template=<path to template>
### flags ### 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"> <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. 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>
<Accordion title="--env"> <Accordion title="--env">
Used to set the environment that secrets are pulled from. Used to set the environment that secrets are pulled from.
@ -162,7 +192,7 @@ infisical export --template=<path to template>
```bash ```bash
# Example # 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. Note: you must reference the tag by its slug name not its fully qualified name. Go to project settings to view all tag slugs.

View File

@ -78,10 +78,7 @@
}, },
{ {
"group": "Infisical SSH", "group": "Infisical SSH",
"pages": [ "pages": ["documentation/platform/ssh/overview", "documentation/platform/ssh/host-groups"]
"documentation/platform/ssh/overview",
"documentation/platform/ssh/host-groups"
]
}, },
{ {
"group": "Key Management (KMS)", "group": "Key Management (KMS)",
@ -378,10 +375,7 @@
}, },
{ {
"group": "Architecture", "group": "Architecture",
"pages": [ "pages": ["internals/architecture/components", "internals/architecture/cloud"]
"internals/architecture/components",
"internals/architecture/cloud"
]
}, },
"internals/security", "internals/security",
"internals/service-tokens" "internals/service-tokens"
@ -491,6 +485,7 @@
"integrations/app-connections/postgres", "integrations/app-connections/postgres",
"integrations/app-connections/railway", "integrations/app-connections/railway",
"integrations/app-connections/render", "integrations/app-connections/render",
"integrations/app-connections/supabase",
"integrations/app-connections/teamcity", "integrations/app-connections/teamcity",
"integrations/app-connections/terraform-cloud", "integrations/app-connections/terraform-cloud",
"integrations/app-connections/vercel", "integrations/app-connections/vercel",
@ -528,6 +523,7 @@
"integrations/secret-syncs/oci-vault", "integrations/secret-syncs/oci-vault",
"integrations/secret-syncs/railway", "integrations/secret-syncs/railway",
"integrations/secret-syncs/render", "integrations/secret-syncs/render",
"integrations/secret-syncs/supabase",
"integrations/secret-syncs/teamcity", "integrations/secret-syncs/teamcity",
"integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/terraform-cloud",
"integrations/secret-syncs/vercel", "integrations/secret-syncs/vercel",
@ -555,10 +551,7 @@
"integrations/cloud/gcp-secret-manager", "integrations/cloud/gcp-secret-manager",
{ {
"group": "Cloudflare", "group": "Cloudflare",
"pages": [ "pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"]
"integrations/cloud/cloudflare-pages",
"integrations/cloud/cloudflare-workers"
]
}, },
"integrations/cloud/terraform-cloud", "integrations/cloud/terraform-cloud",
"integrations/cloud/databricks", "integrations/cloud/databricks",
@ -670,11 +663,7 @@
"cli/commands/reset", "cli/commands/reset",
{ {
"group": "infisical scan", "group": "infisical scan",
"pages": [ "pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"]
"cli/commands/scan",
"cli/commands/scan-git-changes",
"cli/commands/scan-install"
]
} }
] ]
}, },
@ -998,9 +987,7 @@
"pages": [ "pages": [
{ {
"group": "Kubernetes", "group": "Kubernetes",
"pages": [ "pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"]
"api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"
]
}, },
"api-reference/endpoints/dynamic-secrets/create", "api-reference/endpoints/dynamic-secrets/create",
"api-reference/endpoints/dynamic-secrets/update", "api-reference/endpoints/dynamic-secrets/update",
@ -1557,6 +1544,18 @@
"api-reference/endpoints/app-connections/render/delete" "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", "group": "TeamCity",
"pages": [ "pages": [
@ -1908,6 +1907,19 @@
"api-reference/endpoints/secret-syncs/render/remove-secrets" "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", "group": "TeamCity",
"pages": [ "pages": [

View File

@ -42,10 +42,14 @@ To be more specific:
Most of the time, the Infisical server will be behind a load balancer or 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 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 instance, you can configure the TLS to send the client certificate as a header
that is set as an [environment that is set as an [environment variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
</Accordion> </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 ## Guide
In the following steps, we explore how to create and use identities for your workloads and applications on TLS Certificate to 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 clientCertificate = fs.readFileSync("client-cert.pem", "utf8");
const clientKeyCertificate = fs.readFileSync("client-key.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>"; const identityId = "<your-identity-id>";
// Create HTTPS agent with client certificate and key // Create HTTPS agent with client certificate and key

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 KiB

After

Width:  |  Height:  |  Size: 894 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 KiB

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

View File

@ -30,14 +30,14 @@ Infisical supports connecting to PostgreSQL using a database role.
-- enable permissions to alter login credentials -- enable permissions to alter login credentials
ALTER ROLE infisical_role WITH CREATEROLE; ALTER ROLE infisical_role WITH CREATEROLE;
``` ```
<Warning> <Tip>
For each user whose password will be rotated, you must grant that specific user role to your admin user with the ADMIN option: In some configurations, the role performing the rotation must be explicitly granted access to manage each user. To do this, grant the user's role to the rotation role with:
```SQL ```SQL
-- grant each user role to admin user for password rotation -- grant each user role to admin user for password rotation
GRANT <secret-rotation-user> TO <infisical_role> WITH ADMIN OPTION; GRANT <secret_rotation_user> TO <infisical_role> WITH ADMIN OPTION;
``` ```
Replace `<secret-rotation-user>` with each specific username whose credentials will be rotated, and `<infisical_role>` with the role that will perform the rotation. Replace `<secret_rotation_user>` with each specific username whose credentials will be rotated, and `<infisical_role>` with the role that will perform the rotation.
</Warning> </Tip>
</Tab> </Tab>
</Tabs> </Tabs>
</Step> </Step>

View File

@ -0,0 +1,107 @@
---
title: "Supabase Connection"
description: "Learn how to configure a Supabase Connection for Infisical."
---
Infisical supports the use of [Personal Access Tokens](https://supabase.com/dashboard/account/tokens) to connect with Supabase.
## Create a Supabase Personal Access Token
<Steps>
<Step title="Click the profile image in the top-right corner and select 'Account Preferences'">
![Account Preferences](/images/app-connections/supabase/app-connection-user-settings.png)
</Step>
<Step title="In the sidebar, select 'Access Tokens'">
![Settings Page](/images/app-connections/supabase/app-connection-api-keys.png)
</Step>
<Step title="In the access tokens page, click on 'Generate New Token'">
![Access Tokens Page](/images/app-connections/supabase/app-connection-create-api-key.png)
</Step>
<Step title="Enter a token name and click on 'Generate Token'">
Provide a descriptive name for the token.
![Enter Name](/images/app-connections/supabase/app-connection-create-form.png)
</Step>
<Step title="Copy the generated token and save it">
![Create Token](/images/app-connections/supabase/app-connection-key-generated.png)
</Step>
</Steps>
## Create a Supabase Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Select Supabase Connection">
Click **+ Add Connection** and choose **Supabase Connection** from the list of integrations.
![Select Supabase Connection](/images/app-connections/supabase/app-connection-option.png)
</Step>
<Step title="Fill out the Supabase Connection form">
Complete the form by providing:
- A descriptive name for the connection
- An optional description
- Supabase instance URL (e.g., `https://your-domain.com` or `https://api.supabase.com`)
- The Access Token value from the previous step
![Supabase Connection Modal](/images/app-connections/supabase/app-connection-form.png)
</Step>
<Step title="Connection created">
After submitting the form, your **Supabase Connection** will be successfully created and ready to use with your Infisical projects.
![Supabase Connection Created](/images/app-connections/supabase/app-connection-generated.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a Supabase Connection via API, send a request to the [Create Supabase Connection](/api-reference/endpoints/app-connections/supabase/create) endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/supabase \
--header 'Content-Type: application/json' \
--data '{
"name": "my-supabase-connection",
"method": "access-token",
"credentials": {
"accessToken": "[Access Token]",
"instanceUrl": "https://api.supabase.com"
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-supabase-connection",
"description": null,
"version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",
"createdAt": "2025-04-23T19:46:34.831Z",
"updatedAt": "2025-04-23T19:46:34.831Z",
"isPlatformManagedCredentials": false,
"credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f",
"app": "supabase",
"method": "access-token",
"credentials": {
"instanceUrl": "https://api.supabase.com"
}
}
}
```
</Tab>
</Tabs>

View File

@ -0,0 +1,163 @@
---
title: "Supabase Sync"
description: "Learn how to configure a Supabase Sync for Infisical."
---
**Prerequisites:**
- Create a [Supabase Connection](/integrations/app-connections/supabase)
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Add Sync">
Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png)
</Step>
<Step title="Select 'Supabase'">
![Select Supabase](/images/secret-syncs/supabase/select-option.png)
</Step>
<Step title="Configure source">
Configure the **Source** from where secrets should be retrieved, then click **Next**.
![Configure Source](/images/secret-syncs/supabase/sync-source.png)
- **Environment**: The project environment to retrieve secrets from.
- **Secret Path**: The folder path to retrieve secrets from.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
</Step>
<Step title="Configure destination">
Configure the **Destination** to where secrets should be deployed, then click **Next**.
![Configure Destination](/images/secret-syncs/supabase/sync-destination.png)
- **Supabase Connection**: The Supabase Connection to authenticate with.
- **Project**: The Supabase project to sync secrets to.
</Step>
<Step title="Configure Sync Options">
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Options](/images/secret-syncs/supabase/sync-options.png)
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
<Note>
Supabase does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
</Step>
<Step title="Configure details">
Configure the **Details** of your Supabase Sync, then click **Next**.
![Configure Details](/images/secret-syncs/supabase/sync-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
</Step>
<Step title="Review configuration">
Review your Supabase Sync configuration, then click **Create Sync**.
![Review Configuration](/images/secret-syncs/supabase/sync-review.png)
</Step>
<Step title="Sync created">
If enabled, your Supabase Sync will begin syncing your secrets to the destination endpoint.
![Sync Created](/images/secret-syncs/supabase/sync-created.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a **Supabase Sync**, make an API request to the [Create Supabase Sync](/api-reference/endpoints/secret-syncs/supabase/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/supabase \
--header 'Content-Type: application/json' \
--data '{
"name": "my-supabase-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "an example sync",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/my-secrets",
"isEnabled": true,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination",
"autoSyncEnabled": true,
"disableSecretDeletion": false
},
"destinationConfig": {
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"projectName": "Example Project"
}
}'
```
### Sample response
```bash Response
{
"secretSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-supabase-sync",
"description": "an example sync",
"isEnabled": true,
"version": 1,
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"syncStatus": "succeeded",
"lastSyncJobId": "123",
"lastSyncMessage": null,
"lastSyncedAt": "2023-11-07T05:31:56Z",
"importStatus": null,
"lastImportJobId": null,
"lastImportMessage": null,
"lastImportedAt": null,
"removeStatus": null,
"lastRemoveJobId": null,
"lastRemoveMessage": null,
"lastRemovedAt": null,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination",
"autoSyncEnabled": true,
"disableSecretDeletion": false
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connection": {
"app": "supabase",
"name": "my-supabase-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"folder": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"path": "/my-secrets"
},
"destination": "supabase",
"destinationConfig": {
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"projectName": "Example Project"
}
}
}
```
</Tab>
</Tabs>

View File

@ -24,6 +24,7 @@ import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { OCIVaultSyncFields } from "./OCIVaultSyncFields"; import { OCIVaultSyncFields } from "./OCIVaultSyncFields";
import { RailwaySyncFields } from "./RailwaySyncFields"; import { RailwaySyncFields } from "./RailwaySyncFields";
import { RenderSyncFields } from "./RenderSyncFields"; import { RenderSyncFields } from "./RenderSyncFields";
import { SupabaseSyncFields } from "./SupabaseSyncFields";
import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TeamCitySyncFields } from "./TeamCitySyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields"; import { VercelSyncFields } from "./VercelSyncFields";
@ -88,6 +89,8 @@ export const SecretSyncDestinationFields = () => {
return <RailwaySyncFields />; return <RailwaySyncFields />;
case SecretSync.Checkly: case SecretSync.Checkly:
return <ChecklySyncFields />; return <ChecklySyncFields />;
case SecretSync.Supabase:
return <SupabaseSyncFields />;
default: default:
throw new Error(`Unhandled Destination Config Field: ${destination}`); throw new Error(`Unhandled Destination Config Field: ${destination}`);
} }

View File

@ -0,0 +1,65 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl } from "@app/components/v2";
import {
TSupabaseProject,
useSupabaseConnectionListProjects
} from "@app/hooks/api/appConnections/supabase";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const SupabaseSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Supabase }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: projects = [], isPending: isProjectsLoading } = useSupabaseConnectionListProjects(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.projectName", "");
setValue("destinationConfig.projectId", "");
}}
/>
<Controller
name="destinationConfig.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Select a project"
tooltipClassName="max-w-md"
>
<FilterableSelect
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={projects.find((p) => p.id === value) ?? null}
onChange={(option) => {
const v = option as SingleValue<TSupabaseProject>;
onChange(v?.id ?? null);
setValue("destinationConfig.projectName", v?.name ?? "");
}}
options={projects}
placeholder="Select project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
</>
);
};

View File

@ -62,6 +62,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Zabbix: case SecretSync.Zabbix:
case SecretSync.Railway: case SecretSync.Railway:
case SecretSync.Checkly: case SecretSync.Checkly:
case SecretSync.Supabase:
AdditionalSyncOptionsFieldsComponent = null; AdditionalSyncOptionsFieldsComponent = null;
break; break;
default: default:

View File

@ -34,6 +34,7 @@ import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields"; import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields"; import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
import { RenderSyncReviewFields } from "./RenderSyncReviewFields"; import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
@ -140,6 +141,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Checkly: case SecretSync.Checkly:
DestinationFieldsComponent = <ChecklySyncReviewFields />; DestinationFieldsComponent = <ChecklySyncReviewFields />;
break; break;
case SecretSync.Supabase:
DestinationFieldsComponent = <SupabaseSyncReviewFields />;
break;
default: default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`); throw new Error(`Unhandled Destination Review Fields: ${destination}`);
} }

View File

@ -0,0 +1,12 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const SupabaseSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Supabase }>();
const projectName = watch("destinationConfig.projectName");
return <GenericFieldLabel label="Project">{projectName}</GenericFieldLabel>;
};

View File

@ -21,6 +21,7 @@ import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-sch
import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema"; import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema";
import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema"; import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema";
import { RenderSyncDestinationSchema } from "./render-sync-destination-schema"; import { RenderSyncDestinationSchema } from "./render-sync-destination-schema";
import { SupabaseSyncDestinationSchema } from "./supabase-sync-destination-schema";
import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema";
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
@ -51,7 +52,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
GitlabSyncDestinationSchema, GitlabSyncDestinationSchema,
CloudflarePagesSyncDestinationSchema, CloudflarePagesSyncDestinationSchema,
CloudflareWorkersSyncDestinationSchema, CloudflareWorkersSyncDestinationSchema,
SupabaseSyncDestinationSchema,
ZabbixSyncDestinationSchema, ZabbixSyncDestinationSchema,
RailwaySyncDestinationSchema, RailwaySyncDestinationSchema,
ChecklySyncDestinationSchema ChecklySyncDestinationSchema

View File

@ -0,0 +1,14 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const SupabaseSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Supabase),
destinationConfig: z.object({
projectId: z.string().max(255).min(1, "Project ID is required"),
projectName: z.string().max(255).min(1, "Project Name is required")
})
})
);

View File

@ -46,6 +46,7 @@ import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/hero
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection"; import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection";
export const APP_CONNECTION_MAP: Record< export const APP_CONNECTION_MAP: Record<
AppConnection, AppConnection,
@ -96,7 +97,8 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" }, [AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" }, [AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
[AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" } [AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" },
[AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" }
}; };
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@ -151,6 +153,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case HerokuConnectionMethod.AuthToken: case HerokuConnectionMethod.AuthToken:
return { name: "Auth Token", icon: faKey }; return { name: "Auth Token", icon: faKey };
case RailwayConnectionMethod.AccountToken: case RailwayConnectionMethod.AccountToken:
case SupabaseConnectionMethod.AccessToken:
return { name: "Account Token", icon: faKey }; return { name: "Account Token", icon: faKey };
case RailwayConnectionMethod.TeamToken: case RailwayConnectionMethod.TeamToken:
return { name: "Team Token", icon: faKey }; return { name: "Team Token", icon: faKey };

View File

@ -97,6 +97,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.Checkly]: { [SecretSync.Checkly]: {
name: "Checkly", name: "Checkly",
image: "Checkly.png" image: "Checkly.png"
},
[SecretSync.Supabase]: {
name: "Supabase",
image: "Supabase.png"
} }
}; };
@ -124,7 +128,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.GitLab]: AppConnection.Gitlab, [SecretSync.GitLab]: AppConnection.Gitlab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare, [SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Supabase]: AppConnection.Supabase,
[SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway, [SecretSync.Railway]: AppConnection.Railway,
[SecretSync.Checkly]: AppConnection.Checkly [SecretSync.Checkly]: AppConnection.Checkly

View File

@ -31,5 +31,6 @@ export enum AppConnection {
Bitbucket = "bitbucket", Bitbucket = "bitbucket",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",
Checkly = "checkly" Checkly = "checkly",
Supabase = "supabase"
} }

View File

@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";

View File

@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import { TSupabaseProject } from "./types";
const supabaseConnectionKeys = {
all: [...appConnectionKeys.all, "supabase"] as const,
listProjects: (connectionId: string) =>
[...supabaseConnectionKeys.all, "workspace-scopes", connectionId] as const
};
export const useSupabaseConnectionListProjects = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TSupabaseProject[],
unknown,
TSupabaseProject[],
ReturnType<typeof supabaseConnectionKeys.listProjects>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: supabaseConnectionKeys.listProjects(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ projects: TSupabaseProject[] }>(
`/api/v1/app-connections/supabase/${connectionId}/projects`
);
return data.projects;
},
...options
});
};

View File

@ -0,0 +1,4 @@
export type TSupabaseProject = {
id: string;
name: string;
};

View File

@ -148,6 +148,10 @@ export type TChecklyConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Checkly; app: AppConnection.Checkly;
}; };
export type TSupabaseConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Supabase;
};
export type TAppConnectionOption = export type TAppConnectionOption =
| TAwsConnectionOption | TAwsConnectionOption
| TGitHubConnectionOption | TGitHubConnectionOption
@ -215,4 +219,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Zabbix]: TZabbixConnectionOption; [AppConnection.Zabbix]: TZabbixConnectionOption;
[AppConnection.Railway]: TRailwayConnectionOption; [AppConnection.Railway]: TRailwayConnectionOption;
[AppConnection.Checkly]: TChecklyConnectionOption; [AppConnection.Checkly]: TChecklyConnectionOption;
[AppConnection.Supabase]: TSupabaseConnectionOption;
}; };

View File

@ -28,6 +28,7 @@ import { TOracleDBConnection } from "./oracledb-connection";
import { TPostgresConnection } from "./postgres-connection"; import { TPostgresConnection } from "./postgres-connection";
import { TRailwayConnection } from "./railway-connection"; import { TRailwayConnection } from "./railway-connection";
import { TRenderConnection } from "./render-connection"; import { TRenderConnection } from "./render-connection";
import { TSupabaseConnection } from "./supabase-connection";
import { TTeamCityConnection } from "./teamcity-connection"; import { TTeamCityConnection } from "./teamcity-connection";
import { TTerraformCloudConnection } from "./terraform-cloud-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection";
import { TVercelConnection } from "./vercel-connection"; import { TVercelConnection } from "./vercel-connection";
@ -99,7 +100,8 @@ export type TAppConnection =
| TBitbucketConnection | TBitbucketConnection
| TZabbixConnection | TZabbixConnection
| TRailwayConnection | TRailwayConnection
| TChecklyConnection; | TChecklyConnection
| TSupabaseConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">; export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@ -160,4 +162,5 @@ export type TAppConnectionMap = {
[AppConnection.Zabbix]: TZabbixConnection; [AppConnection.Zabbix]: TZabbixConnection;
[AppConnection.Railway]: TRailwayConnection; [AppConnection.Railway]: TRailwayConnection;
[AppConnection.Checkly]: TChecklyConnection; [AppConnection.Checkly]: TChecklyConnection;
[AppConnection.Supabase]: TSupabaseConnection;
}; };

View File

@ -0,0 +1,15 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum SupabaseConnectionMethod {
AccessToken = "access-token"
}
export type TSupabaseConnection = TRootAppConnection & {
app: AppConnection.Supabase;
method: SupabaseConnectionMethod.AccessToken;
credentials: {
instanceUrl?: string;
accessKey: string;
};
};

View File

@ -43,6 +43,7 @@ export type TSecretApprovalRequest = {
isReplicated?: boolean; isReplicated?: boolean;
slug: string; slug: string;
createdAt: string; createdAt: string;
updatedAt: string;
committerUserId: string; committerUserId: string;
reviewers: { reviewers: {
userId: string; userId: string;

View File

@ -22,7 +22,7 @@ export enum SecretSync {
GitLab = "gitlab", GitLab = "gitlab",
CloudflarePages = "cloudflare-pages", CloudflarePages = "cloudflare-pages",
CloudflareWorkers = "cloudflare-workers", CloudflareWorkers = "cloudflare-workers",
Supabase = "supabase",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",
Checkly = "checkly" Checkly = "checkly"

View File

@ -22,6 +22,7 @@ import { THerokuSync } from "./heroku-sync";
import { THumanitecSync } from "./humanitec-sync"; import { THumanitecSync } from "./humanitec-sync";
import { TOCIVaultSync } from "./oci-vault-sync"; import { TOCIVaultSync } from "./oci-vault-sync";
import { TRailwaySync } from "./railway-sync"; import { TRailwaySync } from "./railway-sync";
import { TSupabaseSync } from "./supabase";
import { TTeamCitySync } from "./teamcity-sync"; import { TTeamCitySync } from "./teamcity-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync";
import { TVercelSync } from "./vercel-sync"; import { TVercelSync } from "./vercel-sync";
@ -61,7 +62,8 @@ export type TSecretSync =
| TCloudflareWorkersSync | TCloudflareWorkersSync
| TZabbixSync | TZabbixSync
| TRailwaySync | TRailwaySync
| TChecklySync; | TChecklySync
| TSupabaseSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TSupabaseSync = TRootSecretSync & {
destination: SecretSync.Supabase;
destinationConfig: {
projectId: string;
projectName: string;
};
connection: {
app: AppConnection.Supabase;
name: string;
id: string;
};
};

View File

@ -17,8 +17,8 @@ export type SubscriptionPlan = {
rbac: boolean; rbac: boolean;
secretVersioning: boolean; secretVersioning: boolean;
slug: string; slug: string;
secretApproval: string; secretApproval: boolean;
secretRotation: string; secretRotation: boolean;
tier: number; tier: number;
workspaceLimit: number; workspaceLimit: number;
workspacesUsed: number; workspacesUsed: number;

View File

@ -0,0 +1,60 @@
import { useMemo } from "react";
import { useSubscription, useWorkspace } from "@app/context";
import { useGetAccessApprovalPolicies } from "@app/hooks/api";
const matchesPath = (folderPath: string, pattern: string) => {
const normalizedPath = folderPath === "/" ? "/" : folderPath.replace(/\/$/, "");
const normalizedPattern = pattern === "/" ? "/" : pattern.replace(/\/$/, "");
if (normalizedPath === normalizedPattern) {
return true;
}
if (normalizedPattern.endsWith("/**")) {
const basePattern = normalizedPattern.slice(0, -3); // Remove "/**"
// Handle root wildcard "/**"
if (basePattern === "") {
return true;
}
// Check if path starts with the base pattern
if (normalizedPath === basePattern) {
return true;
}
// Check if path is a subdirectory of the base pattern
return normalizedPath.startsWith(`${basePattern}/`);
}
return false;
};
type Params = {
secretPath: string;
environment: string;
};
export const usePathAccessPolicies = ({ secretPath, environment }: Params) => {
const { currentWorkspace } = useWorkspace();
const { subscription } = useSubscription();
const { data: policies } = useGetAccessApprovalPolicies({
projectSlug: currentWorkspace.slug,
options: {
enabled: subscription.secretApproval
}
});
return useMemo(() => {
const pathPolicies = policies?.filter(
(policy) =>
policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath)
);
return {
hasPathPolicies: subscription.secretApproval && Boolean(pathPolicies?.length),
pathPolicies
};
}, [secretPath, environment, policies, subscription.secretApproval]);
};

View File

@ -31,10 +31,10 @@ enum TimeUnit {
const schema = z.object({ const schema = z.object({
name: z.string().trim().min(1), name: z.string().trim().min(1),
pkiCollectionId: z.string(), pkiCollectionId: z.string().min(1),
alertBefore: z.string(), alertBefore: z.string().min(1),
alertUnit: z.nativeEnum(TimeUnit), alertUnit: z.nativeEnum(TimeUnit),
emails: z.string().trim() emails: z.string().trim().min(1)
}); });
const convertToDays = (unit: TimeUnit, value: number) => { const convertToDays = (unit: TimeUnit, value: number) => {

View File

@ -76,6 +76,7 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
useEffect(() => { useEffect(() => {
if (data) { if (data) {
reset({ reset({
method: EnrollmentMethod.EST,
caChain: data.caChain, caChain: data.caChain,
isEnabled: data.isEnabled, isEnabled: data.isEnabled,
disableBootstrapCertValidation: data.disableBootstrapCertValidation disableBootstrapCertValidation: data.disableBootstrapCertValidation

View File

@ -3,6 +3,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
faCertificate, faCertificate,
faCog,
faEllipsis, faEllipsis,
faPencil, faPencil,
faPlus, faPlus,
@ -12,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
@ -40,12 +42,14 @@ import {
import { import {
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub, ProjectPermissionSub,
useSubscription,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplateV2 } from "@app/hooks/api"; import { useDeleteCertTemplateV2 } from "@app/hooks/api";
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries"; import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
import { PkiTemplateForm } from "./components/PkiTemplateForm"; import { PkiTemplateForm } from "./components/PkiTemplateForm";
const PER_PAGE_INIT = 25; const PER_PAGE_INIT = 25;
@ -56,9 +60,13 @@ export const PkiTemplateListPage = () => {
const [perPage, setPerPage] = useState(PER_PAGE_INIT); const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"certificateTemplate", "certificateTemplate",
"deleteTemplate" "deleteTemplate",
"enrollmentOptions",
"estUpgradePlan"
] as const); ] as const);
const { subscription } = useSubscription();
const { data, isPending } = useListCertificateTemplates({ const { data, isPending } = useListCertificateTemplates({
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
offset: (page - 1) * perPage, offset: (page - 1) * perPage,
@ -92,7 +100,7 @@ export const PkiTemplateListPage = () => {
return ( return (
<> <>
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title> <title>{t("common.head-title", { title: "PKI Templates" })}</title>
</Helmet> </Helmet>
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between text-white"> <div className="container mx-auto flex flex-col justify-between text-white">
@ -177,7 +185,33 @@ export const PkiTemplateListPage = () => {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (!subscription.pkiEst) {
handlePopUpOpen("estUpgradePlan");
return;
}
handlePopUpOpen("enrollmentOptions", {
id: template.id
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faCog} />}
>
Manage Enrollment
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Delete} I={ProjectPermissionPkiTemplateActions.Delete}
a={ProjectPermissionSub.CertificateTemplates} a={ProjectPermissionSub.CertificateTemplates}
@ -251,7 +285,13 @@ export const PkiTemplateListPage = () => {
/> />
</ModalContent> </ModalContent>
</Modal> </Modal>
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div> </div>
<UpgradePlanModal
isOpen={popUp.estUpgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("estUpgradePlan", isOpen)}
text="You can only configure template enrollment methods if you switch to Infisical's Enterprise plan."
/>
</> </>
); );
}; };

View File

@ -37,6 +37,7 @@ import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { RailwayConnectionForm } from "./RailwayConnectionForm"; import { RailwayConnectionForm } from "./RailwayConnectionForm";
import { RenderConnectionForm } from "./RenderConnectionForm"; import { RenderConnectionForm } from "./RenderConnectionForm";
import { SupabaseConnectionForm } from "./SupabaseConnectionForm";
import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TeamCityConnectionForm } from "./TeamCityConnectionForm";
import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
import { VercelConnectionForm } from "./VercelConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm";
@ -146,6 +147,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <RailwayConnectionForm onSubmit={onSubmit} />; return <RailwayConnectionForm onSubmit={onSubmit} />;
case AppConnection.Checkly: case AppConnection.Checkly:
return <ChecklyConnectionForm onSubmit={onSubmit} />; return <ChecklyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} />;
default: default:
throw new Error(`Unhandled App ${app}`); throw new Error(`Unhandled App ${app}`);
} }
@ -248,6 +251,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Checkly: case AppConnection.Checkly:
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default: default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
} }

View File

@ -0,0 +1,159 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
SupabaseConnectionMethod,
TSupabaseConnection
} from "@app/hooks/api/appConnections/types/supabase-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TSupabaseConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Supabase)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(SupabaseConnectionMethod.AccessToken),
credentials: z.object({
accessKey: z.string().trim().min(1, "Access Key required"),
instanceUrl: z.string().url().optional()
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const SupabaseConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Supabase,
method: SupabaseConnectionMethod.AccessToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="credentials.instanceUrl"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isOptional
errorText={error?.message}
isError={Boolean(error?.message)}
label="Instance URL"
tooltipClassName="max-w-sm"
tooltipText="Will default to Supabase Cloud if not specified."
>
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="https://api.supabase.com"
/>
</FormControl>
)}
/>
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The type of token you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Supabase].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Token Type"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(SupabaseConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.accessKey"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Access Key Value"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Supabase"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

Some files were not shown because too many files have changed in this diff Show More