Compare commits

...

10 Commits

Author SHA1 Message Date
Sid
fa00847071 Update docs/docs.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-07-10 22:42:05 +05:30
3c50c2d00a fix: query typo 2025-07-10 21:53:53 +05:30
3d8405d648 fix: sync fields 2025-07-10 21:53:53 +05:30
31a4d1bf3d fix: retry and doc images 2025-07-10 21:53:53 +05:30
05ab55a21b fix: cleanup railway integration 2025-07-10 21:53:53 +05:30
cc8febab41 lint: fix 2025-07-10 21:53:37 +05:30
8aa06302ad fix: undo mock on-prem change 2025-07-10 21:49:33 +05:30
abd23078b8 feat: add documentation on railway 2025-07-10 21:49:33 +05:30
9f34c61c2b fix: railway sync config 2025-07-10 21:49:33 +05:30
0796ee3e5f feat: implement railway secret sync 2025-07-10 21:49:33 +05:30
98 changed files with 1931 additions and 98 deletions

View File

@ -2279,6 +2279,9 @@ export const AppConnections = {
ZABBIX: {
apiToken: "The API Token used to access Zabbix.",
instanceUrl: "The Zabbix instance URL to connect with."
},
RAILWAY: {
apiToken: "The API token used to authenticate with Railway."
}
}
};
@ -2474,6 +2477,14 @@ export const SecretSyncs = {
hostId: "The ID of the Zabbix host to sync secrets to.",
hostName: "The name of the Zabbix host to sync secrets to.",
macroType: "The type of macro to sync secrets to. (0: Text, 1: Secret)"
},
RAILWAY: {
projectId: "The ID of the Railway project to sync secrets to.",
projectName: "The name of the Railway project to sync secrets to.",
environmentId: "The Railway environment to sync secrets to.",
environmentName: "The Railway environment to sync secrets to.",
serviceId: "The Railway service that secrets should be synced to.",
serviceName: "The Railway service that secrets should be synced to."
}
}
};
@ -2594,7 +2605,9 @@ export const SecretRotations = {
export const SecretScanningDataSources = {
LIST: (type?: SecretScanningDataSource) => ({
projectId: `The ID of the project to list ${type ? SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type] : "Scanning"} Data Sources from.`
projectId: `The ID of the project to list ${
type ? SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type] : "Scanning"
} Data Sources from.`
}),
GET_BY_ID: (type: SecretScanningDataSource) => ({
dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to retrieve.`

View File

@ -1,11 +1,18 @@
import axios from "axios";
import axiosRetry from "axios-retry";
import axios, { AxiosInstance, CreateAxiosDefaults } from "axios";
import axiosRetry, { IAxiosRetryConfig } from "axios-retry";
export const request = axios.create();
export function createRequestClient(defaults: CreateAxiosDefaults = {}, retry: IAxiosRetryConfig = {}): AxiosInstance {
const client = axios.create(defaults);
axiosRetry(request, {
retries: 3,
// eslint-disable-next-line
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (err) => axiosRetry.isNetworkError(err) || axiosRetry.isRetryableError(err)
});
axiosRetry(client, {
retries: 3,
// eslint-disable-next-line
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (err) => axiosRetry.isNetworkError(err) || axiosRetry.isRetryableError(err),
...retry
});
return client;
}
export const request = createRequestClient();

View File

@ -71,6 +71,10 @@ import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import {
RailwayConnectionListItemSchema,
SanitizedRailwayConnectionSchema
} from "@app/services/app-connection/railway";
import {
RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema
@ -123,7 +127,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedGitLabConnectionSchema.options,
...SanitizedCloudflareConnectionSchema.options,
...SanitizedBitbucketConnectionSchema.options,
...SanitizedZabbixConnectionSchema.options
...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -157,7 +162,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
GitLabConnectionListItemSchema,
CloudflareConnectionListItemSchema,
BitbucketConnectionListItemSchema,
ZabbixConnectionListItemSchema
ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@ -25,6 +25,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
@ -66,5 +67,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter
};

View File

@ -0,0 +1,67 @@
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 {
CreateRailwayConnectionSchema,
SanitizedRailwayConnectionSchema,
UpdateRailwayConnectionSchema
} from "@app/services/app-connection/railway";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerRailwayConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Railway,
server,
sanitizedResponseSchema: SanitizedRailwayConnectionSchema,
createSchema: CreateRailwayConnectionSchema,
updateSchema: UpdateRailwayConnectionSchema
});
// 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(),
services: z.array(
z.object({
name: z.string(),
id: z.string()
})
),
environments: z.array(
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.railway.listProjects(connectionId, req.permission);
return { projects };
}
});
};

View File

@ -17,6 +17,7 @@ import { registerGitLabSyncRouter } from "./gitlab-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
@ -49,5 +50,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Flyio]: registerFlyioSyncRouter,
[SecretSync.GitLab]: registerGitLabSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter,
[SecretSync.Zabbix]: registerZabbixSyncRouter
[SecretSync.Zabbix]: registerZabbixSyncRouter,
[SecretSync.Railway]: registerRailwaySyncRouter
};

View File

@ -0,0 +1,17 @@
import {
CreateRailwaySyncSchema,
RailwaySyncSchema,
UpdateRailwaySyncSchema
} from "@app/services/secret-sync/railway/railway-sync-schemas";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerRailwaySyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Railway,
server,
responseSchema: RailwaySyncSchema,
createSchema: CreateRailwaySyncSchema,
updateSchema: UpdateRailwaySyncSchema
});

View File

@ -34,6 +34,7 @@ import { GitLabSyncListItemSchema, GitLabSyncSchema } from "@app/services/secret
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
@ -64,7 +65,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
FlyioSyncSchema,
GitLabSyncSchema,
CloudflarePagesSyncSchema,
ZabbixSyncSchema
ZabbixSyncSchema,
RailwaySyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@ -90,7 +92,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
FlyioSyncListItemSchema,
GitLabSyncListItemSchema,
CloudflarePagesSyncListItemSchema,
ZabbixSyncListItemSchema
ZabbixSyncListItemSchema,
RailwaySyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@ -28,8 +28,9 @@ export enum AppConnection {
Flyio = "flyio",
GitLab = "gitlab",
Cloudflare = "cloudflare",
Bitbucket = "bitbucket",
Zabbix = "zabbix"
Zabbix = "zabbix",
Railway = "railway",
Bitbucket = "bitbucket"
}
export enum AWSRegion {

View File

@ -91,6 +91,7 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import {
@ -143,8 +144,9 @@ export const listAppConnectionOptions = () => {
getFlyioConnectionListItem(),
getGitLabConnectionListItem(),
getCloudflareConnectionListItem(),
getBitbucketConnectionListItem(),
getZabbixConnectionListItem()
getZabbixConnectionListItem(),
getRailwayConnectionListItem(),
getBitbucketConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@ -225,8 +227,9 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@ -345,8 +348,9 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (

View File

@ -30,8 +30,9 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Zabbix]: "Zabbix"
[AppConnection.Zabbix]: "Zabbix",
[AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@ -64,6 +65,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Zabbix]: AppConnectionPlanType.Regular
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
[AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular
};

View File

@ -72,6 +72,8 @@ import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
@ -124,8 +126,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@ -536,7 +539,8 @@ export const appConnectionServiceFactory = ({
flyio: flyioConnectionService(connectAppConnectionById),
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
cloudflare: cloudflareConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById),
zabbix: zabbixConnectionService(connectAppConnectionById)
zabbix: zabbixConnectionService(connectAppConnectionById),
railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById)
};
};

View File

@ -141,6 +141,12 @@ import {
TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema
} from "./postgres";
import {
TRailwayConnection,
TRailwayConnectionConfig,
TRailwayConnectionInput,
TValidateRailwayConnectionCredentialsSchema
} from "./railway";
import {
TRenderConnection,
TRenderConnectionConfig,
@ -210,6 +216,7 @@ export type TAppConnection = { id: string } & (
| TCloudflareConnection
| TBitbucketConnection
| TZabbixConnection
| TRailwayConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@ -248,6 +255,7 @@ export type TAppConnectionInput = { id: string } & (
| TCloudflareConnectionInput
| TBitbucketConnectionInput
| TZabbixConnectionInput
| TRailwayConnectionInput
);
export type TSqlConnectionInput =
@ -293,7 +301,8 @@ export type TAppConnectionConfig =
| TGitLabConnectionConfig
| TCloudflareConnectionConfig
| TBitbucketConnectionConfig
| TZabbixConnectionConfig;
| TZabbixConnectionConfig
| TRailwayConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@ -326,7 +335,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema
| TValidateBitbucketConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema;
| TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;

View File

@ -0,0 +1,4 @@
export * from "./railway-connection-constants";
export * from "./railway-connection-fns";
export * from "./railway-connection-schemas";
export * from "./railway-connection-types";

View File

@ -0,0 +1,5 @@
export enum RailwayConnectionMethod {
AccountToken = "account-token",
ProjectToken = "project-token",
TeamToken = "team-token"
}

View File

@ -0,0 +1,66 @@
/* 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 { RailwayConnectionMethod } from "./railway-connection-constants";
import { RailwayPublicAPI } from "./railway-connection-public-client";
import { TRailwayConnection, TRailwayConnectionConfig } from "./railway-connection-types";
export const getRailwayConnectionListItem = () => {
return {
name: "Railway" as const,
app: AppConnection.Railway as const,
methods: Object.values(RailwayConnectionMethod)
};
};
export const validateRailwayConnectionCredentials = async (config: TRailwayConnectionConfig) => {
const { credentials, method } = config;
try {
await RailwayPublicAPI.healthcheck({
method,
credentials
});
} 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: TRailwayConnection) => {
const { credentials, method } = appConnection;
try {
return await RailwayPublicAPI.listProjects({
method,
credentials
});
} 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,237 @@
/* eslint-disable class-methods-use-this */
import { AxiosError, AxiosInstance, AxiosResponse } from "axios";
import { createRequestClient } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { RailwayConnectionMethod } from "./railway-connection-constants";
import {
RailwayAccountWorkspaceListSchema,
RailwayGetProjectsByProjectTokenSchema,
RailwayGetSubscriptionTypeSchema,
RailwayProjectsListSchema
} from "./railway-connection-schemas";
import { RailwayProject, TRailwayConnectionConfig, TRailwayResponse } from "./railway-connection-types";
type RailwaySendReqOptions = Pick<TRailwayConnectionConfig, "credentials" | "method">;
export function getRailwayAuthHeaders(method: RailwayConnectionMethod, token: string): Record<string, string> {
switch (method) {
case RailwayConnectionMethod.AccountToken:
case RailwayConnectionMethod.TeamToken:
return {
Authorization: token
};
case RailwayConnectionMethod.ProjectToken:
return {
"Project-Access-Token": token
};
default:
throw new Error(`Unsupported Railway connection method`);
}
}
export function getRailwayRatelimiter(headers: AxiosResponse["headers"]): {
isRatelimited: boolean;
maxAttempts: number;
wait: () => Promise<void>;
} {
const retryAfter: number | undefined = headers["Retry-After"] as number | undefined;
const requestsLeft = parseInt(headers["X-RateLimit-Remaining"] as string, 10);
const limitResetAt = headers["X-RateLimit-Reset"] as string;
const now = +new Date();
const nextReset = +new Date(limitResetAt);
const remaining = Math.min(0, nextReset - now);
const wait = () => {
return new Promise<void>((res) => {
setTimeout(res, remaining);
});
};
return {
isRatelimited: Boolean(retryAfter || requestsLeft === 0),
wait,
maxAttempts: 3
};
}
class RailwayPublicClient {
private client: AxiosInstance;
constructor() {
this.client = createRequestClient({
method: "POST",
baseURL: IntegrationUrls.RAILWAY_API_URL,
headers: {
"Content-Type": "application/json"
}
});
}
async send<T extends TRailwayResponse>(
query: string,
options: RailwaySendReqOptions,
variables: Record<string, string | Record<string, string>> = {},
retryAttempt: number = 0
): Promise<T["data"] | undefined> {
const body = {
query,
variables
};
const response = await this.client.request<T>({
data: body,
headers: getRailwayAuthHeaders(options.method, options.credentials.apiToken)
});
const { errors } = response.data;
if (Array.isArray(errors) && errors.length > 0) {
throw new AxiosError(errors[0].message);
}
const limiter = getRailwayRatelimiter(response.headers);
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
await limiter.wait();
return this.send(query, options, variables, retryAttempt + 1);
}
return response.data.data;
}
healthcheck(config: RailwaySendReqOptions) {
switch (config.method) {
case RailwayConnectionMethod.AccountToken:
return this.send(`{ me { teams { edges { node { id } } } } }`, config);
case RailwayConnectionMethod.ProjectToken:
return this.send(`{ projectToken { projectId environmentId project { id } } }`, config);
case RailwayConnectionMethod.TeamToken:
return this.send(`{ projects { edges { node { id name team { id } } } } }`, config);
default:
throw new Error(`Unsupported Railway connection method`);
}
}
async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) {
const res = await this.send(
`query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`,
config,
{
projectId: config.projectId
}
);
const data = await RailwayGetSubscriptionTypeSchema.parseAsync(res);
return data.project.subscriptionType;
}
async listProjects(config: RailwaySendReqOptions): Promise<RailwayProject[]> {
switch (config.method) {
case RailwayConnectionMethod.TeamToken: {
const res = await this.send(
`{ projects { edges { node { id, name, services{ edges{ node { id, name } } } environments { edges { node { name, id } } } } } } }`,
config
);
const data = await RailwayProjectsListSchema.parseAsync(res);
return data.projects.edges.map((p) => ({
id: p.node.id,
name: p.node.name,
environments: p.node.environments.edges.map((e) => e.node),
services: p.node.services.edges.map((s) => s.node)
}));
}
case RailwayConnectionMethod.AccountToken: {
const res = await this.send(
`{ me { workspaces { id, name, team{ projects{ edges{ node{ id, name, services{ edges { node { name, id } } } environments { edges { node { name, id } } } } } } } } } }`,
config
);
const data = await RailwayAccountWorkspaceListSchema.parseAsync(res);
return data.me.workspaces.flatMap((w) =>
w.team.projects.edges.map((p) => ({
id: p.node.id,
name: p.node.name,
environments: p.node.environments.edges.map((e) => e.node),
services: p.node.services.edges.map((s) => s.node)
}))
);
}
case RailwayConnectionMethod.ProjectToken: {
const res = await this.send(
`query { projectToken { project { id, name, services { edges { node { name, id } } } environments { edges { node { name, id } } } } } }`,
config
);
const data = await RailwayGetProjectsByProjectTokenSchema.parseAsync(res);
const p = data.projectToken.project;
return [
{
id: p.id,
name: p.name,
environments: p.environments.edges.map((e) => e.node),
services: p.services.edges.map((s) => s.node)
}
];
}
default:
throw new Error(`Unsupported Railway connection method`);
}
}
async getVariables(
config: RailwaySendReqOptions,
variables: { projectId: string; environmentId: string; serviceId?: string }
) {
const res = await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
`query variables($environmentId: String!, $projectId: String!, $serviceId: String) { variables( projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId ) }`,
config,
variables
);
if (!res?.variables) {
throw new BadRequestError({
message: "Failed to get railway variables - empty response"
});
}
return res.variables;
}
async deleteVariable(
config: RailwaySendReqOptions,
variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } }
) {
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
`mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
config,
variables
);
}
async upsertVariable(
config: RailwaySendReqOptions,
variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } }
) {
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
`mutation variableUpsert($input: VariableUpsertInput!) { variableUpsert(input: $input) }`,
config,
variables
);
}
}
export const RailwayPublicAPI = new RailwayPublicClient();

View File

@ -0,0 +1,117 @@
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 { RailwayConnectionMethod } from "./railway-connection-constants";
export const RailwayConnectionMethodSchema = z
.nativeEnum(RailwayConnectionMethod)
.describe(AppConnections.CREATE(AppConnection.Railway).method);
export const RailwayConnectionAccessTokenCredentialsSchema = z.object({
apiToken: z
.string()
.trim()
.min(1, "API Token required")
.max(255)
.describe(AppConnections.CREDENTIALS.RAILWAY.apiToken)
});
const BaseRailwayConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Railway)
});
export const RailwayConnectionSchema = BaseRailwayConnectionSchema.extend({
method: RailwayConnectionMethodSchema,
credentials: RailwayConnectionAccessTokenCredentialsSchema
});
export const SanitizedRailwayConnectionSchema = z.discriminatedUnion("method", [
BaseRailwayConnectionSchema.extend({
method: RailwayConnectionMethodSchema,
credentials: RailwayConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateRailwayConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: RailwayConnectionMethodSchema,
credentials: RailwayConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Railway).credentials
)
})
]);
export const CreateRailwayConnectionSchema = ValidateRailwayConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Railway)
);
export const UpdateRailwayConnectionSchema = z
.object({
credentials: RailwayConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Railway).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Railway));
export const RailwayConnectionListItemSchema = z.object({
name: z.literal("Railway"),
app: z.literal(AppConnection.Railway),
methods: z.nativeEnum(RailwayConnectionMethod).array()
});
export const RailwayResourceSchema = z.object({
node: z.object({
id: z.string(),
name: z.string()
})
});
export const RailwayProjectEdgeSchema = z.object({
node: z.object({
id: z.string(),
name: z.string(),
services: z.object({
edges: z.array(RailwayResourceSchema)
}),
environments: z.object({
edges: z.array(RailwayResourceSchema)
})
})
});
export const RailwayProjectsListSchema = z.object({
projects: z.object({
edges: z.array(RailwayProjectEdgeSchema)
})
});
export const RailwayAccountWorkspaceListSchema = z.object({
me: z.object({
workspaces: z.array(
z.object({
id: z.string(),
name: z.string(),
team: RailwayProjectsListSchema
})
)
})
});
export const RailwayGetProjectsByProjectTokenSchema = z.object({
projectToken: z.object({
project: RailwayProjectEdgeSchema.shape.node
})
});
export const RailwayGetSubscriptionTypeSchema = z.object({
project: z.object({
subscriptionType: z.enum(["free", "hobby", "pro", "trial"])
})
});

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 getRailwayProjects } from "./railway-connection-fns";
import { TRailwayConnection } from "./railway-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TRailwayConnection>;
export const railwayConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Railway, connectionId, actor);
try {
const projects = await getRailwayProjects(appConnection);
return projects;
} catch (error) {
logger.error(error, "Failed to establish connection with Railway");
return [];
}
};
return {
listProjects
};
};

View File

@ -0,0 +1,79 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateRailwayConnectionSchema,
RailwayConnectionSchema,
ValidateRailwayConnectionCredentialsSchema
} from "./railway-connection-schemas";
export type TRailwayConnection = z.infer<typeof RailwayConnectionSchema>;
export type TRailwayConnectionInput = z.infer<typeof CreateRailwayConnectionSchema> & {
app: AppConnection.Railway;
};
export type TValidateRailwayConnectionCredentialsSchema = typeof ValidateRailwayConnectionCredentialsSchema;
export type TRailwayConnectionConfig = DiscriminativePick<TRailwayConnection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TRailwayService = {
id: string;
name: string;
};
export type TRailwayEnvironment = {
id: string;
name: string;
};
export type RailwayProject = {
id: string;
name: string;
services: TRailwayService[];
environments: TRailwayEnvironment[];
};
export type TRailwayResponse<T = unknown> = {
data?: T;
errors?: {
message: string;
}[];
};
export type TAccountProjectListResponse = TRailwayResponse<{
projects: {
edges: TProjectEdge[];
};
}>;
export interface TProjectEdge {
node: {
id: string;
name: string;
services: {
edges: TServiceEdge[];
};
environments: {
edges: TEnvironmentEdge[];
};
};
}
type TServiceEdge = {
node: {
id: string;
name: string;
};
};
type TEnvironmentEdge = {
node: {
id: string;
name: string;
};
};

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 RAILWAY_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Railway",
destination: SecretSync.Railway,
connection: AppConnection.Railway,
canImportSecrets: true
};

View File

@ -0,0 +1,124 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { RailwayPublicAPI } from "@app/services/app-connection/railway/railway-connection-public-client";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { TSecretMap } from "../secret-sync-types";
import { TRailwaySyncWithCredentials } from "./railway-sync-types";
export const RailwaySyncFns = {
async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise<TSecretMap> {
try {
const config = secretSync.destinationConfig;
const variables = await RailwayPublicAPI.getVariables(secretSync.connection, {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined
});
const entries = {} as TSecretMap;
for (const [key, value] of Object.entries(variables)) {
// Skip importing private railway variables
// eslint-disable-next-line no-continue
if (key.startsWith("RAILWAY_")) continue;
entries[key] = {
value
};
}
return entries;
} catch (error) {
throw new SecretSyncError({
error,
message: "Failed to import secrets from Railway"
});
}
},
async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const railwaySecrets = await this.getSecrets(secretSync);
const config = secretSync.destinationConfig;
for await (const key of Object.keys(secretMap)) {
try {
const existing = railwaySecrets[key];
if (existing === undefined || existing.value !== secretMap[key].value) {
await RailwayPublicAPI.upsertVariable(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
name: key,
value: secretMap[key].value ?? ""
}
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (disableSecretDeletion) return;
for await (const key of Object.keys(railwaySecrets)) {
try {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
if (!secretMap[key]) {
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
name: key
}
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
},
async removeSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
const existing = await this.getSecrets(secretSync);
const config = secretSync.destinationConfig;
for await (const secret of Object.keys(existing)) {
try {
if (secret in secretMap) {
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
name: secret
}
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: secret
});
}
}
}
};

View File

@ -0,0 +1,56 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
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 RailwaySyncDestinationConfigSchema = z.object({
projectId: z
.string()
.trim()
.min(1, "Railway project ID required")
.describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.projectId),
projectName: z.string().trim().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.projectName),
environmentId: z
.string()
.trim()
.min(1, "Railway environment ID required")
.describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.environmentId),
environmentName: z.string().trim().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.environmentName),
serviceId: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.serviceId),
serviceName: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.serviceName)
});
const RailwaySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const RailwaySyncSchema = BaseSecretSyncSchema(SecretSync.Railway, RailwaySyncOptionsConfig).extend({
destination: z.literal(SecretSync.Railway),
destinationConfig: RailwaySyncDestinationConfigSchema
});
export const CreateRailwaySyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Railway,
RailwaySyncOptionsConfig
).extend({
destinationConfig: RailwaySyncDestinationConfigSchema
});
export const UpdateRailwaySyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Railway,
RailwaySyncOptionsConfig
).extend({
destinationConfig: RailwaySyncDestinationConfigSchema.optional()
});
export const RailwaySyncListItemSchema = z.object({
name: z.literal("Railway"),
connection: z.literal(AppConnection.Railway),
destination: z.literal(SecretSync.Railway),
canImportSecrets: z.literal(true)
});

View File

@ -0,0 +1,31 @@
import z from "zod";
import { TRailwayConnection } from "@app/services/app-connection/railway";
import { CreateRailwaySyncSchema, RailwaySyncListItemSchema, RailwaySyncSchema } from "./railway-sync-schemas";
export type TRailwaySyncListItem = z.infer<typeof RailwaySyncListItemSchema>;
export type TRailwaySync = z.infer<typeof RailwaySyncSchema>;
export type TRailwaySyncInput = z.infer<typeof CreateRailwaySyncSchema>;
export type TRailwaySyncWithCredentials = TRailwaySync & {
connection: TRailwayConnection;
};
export type TRailwaySecret = {
createdAt: string;
environmentId?: string | null;
id: string;
isSealed: boolean;
name: string;
serviceId?: string | null;
updatedAt: string;
};
export type TRailwayVariablesGraphResponse = {
data: {
variables: Record<string, string>;
};
};

View File

@ -21,7 +21,8 @@ export enum SecretSync {
Flyio = "flyio",
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages",
Zabbix = "zabbix"
Zabbix = "zabbix",
Railway = "railway"
}
export enum SecretSyncInitialSyncBehavior {

View File

@ -39,6 +39,8 @@ import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
import { RailwaySyncFns } from "./railway/railway-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps";
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
@ -70,7 +72,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION,
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION,
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@ -240,6 +243,8 @@ export const SecretSyncFns = {
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Zabbix:
return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway:
return RailwaySyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@ -335,6 +340,9 @@ export const SecretSyncFns = {
case SecretSync.Zabbix:
secretMap = await ZabbixSyncFns.getSecrets(secretSync);
break;
case SecretSync.Railway:
secretMap = await RailwaySyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@ -414,6 +422,8 @@ export const SecretSyncFns = {
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Zabbix:
return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway:
return RailwaySyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@ -24,7 +24,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Flyio]: "Fly.io",
[SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages",
[SecretSync.Zabbix]: "Zabbix"
[SecretSync.Zabbix]: "Zabbix",
[SecretSync.Railway]: "Railway"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@ -50,7 +51,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.Zabbix]: AppConnection.Zabbix
[SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway
};
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
@ -76,5 +78,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular,
[SecretSync.Zabbix]: SecretSyncPlanType.Regular
[SecretSync.Zabbix]: SecretSyncPlanType.Regular,
[SecretSync.Railway]: SecretSyncPlanType.Regular
};

View File

@ -94,6 +94,12 @@ import {
THumanitecSyncListItem,
THumanitecSyncWithCredentials
} from "./humanitec";
import {
TRailwaySync,
TRailwaySyncInput,
TRailwaySyncListItem,
TRailwaySyncWithCredentials
} from "./railway/railway-sync-types";
import {
TRenderSync,
TRenderSyncInput,
@ -138,7 +144,8 @@ export type TSecretSync =
| TFlyioSync
| TGitLabSync
| TCloudflarePagesSync
| TZabbixSync;
| TZabbixSync
| TRailwaySync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@ -163,7 +170,8 @@ export type TSecretSyncWithCredentials =
| TFlyioSyncWithCredentials
| TGitLabSyncWithCredentials
| TCloudflarePagesSyncWithCredentials
| TZabbixSyncWithCredentials;
| TZabbixSyncWithCredentials
| TRailwaySyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@ -188,7 +196,8 @@ export type TSecretSyncInput =
| TFlyioSyncInput
| TGitLabSyncInput
| TCloudflarePagesSyncInput
| TZabbixSyncInput;
| TZabbixSyncInput
| TRailwaySyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@ -213,7 +222,8 @@ export type TSecretSyncListItem =
| TFlyioSyncListItem
| TGitLabSyncListItem
| TCloudflarePagesSyncListItem
| TZabbixSyncListItem;
| TZabbixSyncListItem
| TRailwaySyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

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

View File

@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/railway"
---
<Note>
Check out the configuration docs for [Railway Connections](/integrations/app-connections/railway) to learn how to obtain the required credentials.
</Note>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/railway/{connectionId}"
---
<Note>
Check out the configuration docs for [Railway Connections](/integrations/app-connections/railway) to learn how to obtain the required credentials.
</Note>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/railway/{syncId}/import-secrets"
---

View File

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

View File

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

View File

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

View File

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

View File

@ -78,10 +78,7 @@
},
{
"group": "Infisical SSH",
"pages": [
"documentation/platform/ssh/overview",
"documentation/platform/ssh/host-groups"
]
"pages": ["documentation/platform/ssh/overview", "documentation/platform/ssh/host-groups"]
},
{
"group": "Key Management (KMS)",
@ -378,10 +375,7 @@
},
{
"group": "Architecture",
"pages": [
"internals/architecture/components",
"internals/architecture/cloud"
]
"pages": ["internals/architecture/components", "internals/architecture/cloud"]
},
"internals/security",
"internals/service-tokens"
@ -488,6 +482,7 @@
"integrations/app-connections/oci",
"integrations/app-connections/oracledb",
"integrations/app-connections/postgres",
"integrations/app-connections/railway",
"integrations/app-connections/render",
"integrations/app-connections/teamcity",
"integrations/app-connections/terraform-cloud",
@ -522,6 +517,7 @@
"integrations/secret-syncs/heroku",
"integrations/secret-syncs/humanitec",
"integrations/secret-syncs/oci-vault",
"integrations/secret-syncs/railway",
"integrations/secret-syncs/render",
"integrations/secret-syncs/teamcity",
"integrations/secret-syncs/terraform-cloud",
@ -550,10 +546,7 @@
"integrations/cloud/gcp-secret-manager",
{
"group": "Cloudflare",
"pages": [
"integrations/cloud/cloudflare-pages",
"integrations/cloud/cloudflare-workers"
]
"pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"]
},
"integrations/cloud/terraform-cloud",
"integrations/cloud/databricks",
@ -563,8 +556,8 @@
"integrations/cloud/digital-ocean-app-platform",
"integrations/cloud/heroku",
"integrations/cloud/netlify",
"integrations/cloud/railway",
"integrations/cloud/flyio",
"integrations/cloud/railway",
"integrations/cloud/render",
"integrations/cloud/laravel-forge",
"integrations/cloud/supabase",
@ -665,11 +658,7 @@
"cli/commands/reset",
{
"group": "infisical scan",
"pages": [
"cli/commands/scan",
"cli/commands/scan-git-changes",
"cli/commands/scan-install"
]
"pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"]
}
]
},
@ -853,30 +842,30 @@
{
"group": "Organizations",
"pages": [
{
"group": "OIDC SSO",
"pages": [
"api-reference/endpoints/organizations/oidc-sso/get-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/update-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/create-oidc-config"
]
},
{
"group": "LDAP SSO",
"pages": [
"api-reference/endpoints/organizations/ldap-sso/get-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/update-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/create-ldap-config"
]
},
{
"group": "SAML SSO",
"pages": [
"api-reference/endpoints/organizations/saml-sso/get-saml-config",
"api-reference/endpoints/organizations/saml-sso/update-saml-config",
"api-reference/endpoints/organizations/saml-sso/create-saml-config"
]
},
{
"group": "OIDC SSO",
"pages": [
"api-reference/endpoints/organizations/oidc-sso/get-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/update-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/create-oidc-config"
]
},
{
"group": "LDAP SSO",
"pages": [
"api-reference/endpoints/organizations/ldap-sso/get-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/update-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/create-ldap-config"
]
},
{
"group": "SAML SSO",
"pages": [
"api-reference/endpoints/organizations/saml-sso/get-saml-config",
"api-reference/endpoints/organizations/saml-sso/update-saml-config",
"api-reference/endpoints/organizations/saml-sso/create-saml-config"
]
},
"api-reference/endpoints/organizations/memberships",
"api-reference/endpoints/organizations/update-membership",
"api-reference/endpoints/organizations/delete-membership",
@ -993,9 +982,7 @@
"pages": [
{
"group": "Kubernetes",
"pages": [
"api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"
]
"pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"]
},
"api-reference/endpoints/dynamic-secrets/create",
"api-reference/endpoints/dynamic-secrets/update",
@ -1517,6 +1504,18 @@
"api-reference/endpoints/app-connections/postgres/delete"
]
},
{
"group": "Railway",
"pages": [
"api-reference/endpoints/app-connections/railway/list",
"api-reference/endpoints/app-connections/railway/available",
"api-reference/endpoints/app-connections/railway/get-by-id",
"api-reference/endpoints/app-connections/railway/get-by-name",
"api-reference/endpoints/app-connections/railway/create",
"api-reference/endpoints/app-connections/railway/update",
"api-reference/endpoints/app-connections/railway/delete"
]
},
{
"group": "Render",
"pages": [
@ -1826,6 +1825,20 @@
"api-reference/endpoints/secret-syncs/oci-vault/remove-secrets"
]
},
{
"group": "Railway",
"pages": [
"api-reference/endpoints/secret-syncs/railway/list",
"api-reference/endpoints/secret-syncs/railway/get-by-id",
"api-reference/endpoints/secret-syncs/railway/get-by-name",
"api-reference/endpoints/secret-syncs/railway/create",
"api-reference/endpoints/secret-syncs/railway/update",
"api-reference/endpoints/secret-syncs/railway/delete",
"api-reference/endpoints/secret-syncs/railway/sync-secrets",
"api-reference/endpoints/secret-syncs/railway/import-secrets",
"api-reference/endpoints/secret-syncs/railway/remove-secrets"
]
},
{
"group": "Render",
"pages": [
@ -2172,7 +2185,7 @@
"api": {
"openapi": "https://app.infisical.com/api/docs/json",
"mdx": {
"server": ["https://app.infisical.com", "http://localhost:8080"]
"server": ["https://app.infisical.com"]
}
},
"appearance": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

View File

@ -0,0 +1,164 @@
---
title: "Railway Connection"
description: "Learn how to configure a Railway Connection for Infisical."
---
Infisical supports the use of [API Tokens](https://docs.railway.com/guides/public-api#creating-a-token) to connect with Railway.
## Create a Railway API Token
<Tabs>
<Tab title="Team Token">
A team token provides access to all resources within a team. It cannot be used to access personal resources in Railway.
<Steps>
<Step title="Click the profile image in the top-right corner and select 'Account Settings'">
![Dashboard Page](/images/app-connections/railway/railway-app-connection-account-settings.png)
</Step>
<Step title="In the personal settings sidebar, click on 'Tokens'">
![Account Settings Page](/images/app-connections/railway/railway-app-connection-account-settings-tokens.png)
</Step>
<Step title="Enter a token name and select a team">
Make sure to provide a descriptive name and select the correct team.
![Enter Name and Select Team](/images/app-connections/railway/railway-app-connection-team-token-form.png)
</Step>
<Step title="Click on 'Create'">
![Create Token](/images/app-connections/railway/railway-app-connection-team-token-create.png)
</Step>
<Step title="Save the token">
After clicking 'Create', your access token will be displayed. Save it securely for later use.
![Copy Token Modal](/images/app-connections/railway/railway-app-connection-team-token-created.png)
</Step>
</Steps>
</Tab>
<Tab title="Account Token">
If no team is selected, the token will be associated with your personal Railway account and will have access to all your individual and team resources.
<Steps>
<Step title="Click the profile image in the top-right corner and select 'Account Settings'">
![Dashboard Page](/images/app-connections/railway/railway-app-connection-account-settings.png)
</Step>
<Step title="In the personal settings sidebar, click on 'Tokens'">
![Account Settings Page](/images/app-connections/railway/railway-app-connection-account-settings-tokens.png)
</Step>
<Step title="Enter a token name and select 'No workspace'">
Provide a descriptive name and ensure no team is selected. This will create an account-level token.
![Enter Name](/images/app-connections/railway/railway-app-connection-account-token-form.png)
</Step>
<Step title="Click on 'Create'">
![Create Token](/images/app-connections/railway/railway-app-connection-account-token-create.png)
</Step>
<Step title="Save the token">
After clicking 'Create', your access token will be shown. Save it for future use.
![Copy Token Modal](/images/app-connections/railway/railway-app-connection-account-token-created.png)
</Step>
</Steps>
</Tab>
<Tab title="Project Token">
Project tokens are limited to a specific environment within a project and can only be used to authenticate requests to that environment.
<Steps>
<Step title="Open your Railway dashboard and click on a project">
![Dashboard Page](/images/app-connections/railway/railway-app-connection-project-token-dashboard.png)
</Step>
<Step title="On the project page, click 'Settings' in the top-right corner">
![Project Settings Page](/images/app-connections/railway/railway-app-connection-project-token-project.png)
</Step>
<Step title="In the left sidebar, scroll to the bottom and click on 'Tokens'">
![Project Token Settings Page](/images/app-connections/railway/railway-app-connection-project-token-settings.png)
</Step>
<Step title="Enter a token name and select an environment">
Provide a descriptive name and select the appropriate environment for the token.
![Enter Name and Select environment](/images/app-connections/railway/railway-app-connection-project-token-form.png)
</Step>
<Step title="Click on 'Create'">
![Create Token](/images/app-connections/railway/railway-app-connection-project-token-create.png)
</Step>
<Step title="Save the token">
After clicking 'Create', the access token will be displayed. Be sure to save it for later use.
![Copy Token Modal](/images/app-connections/railway/railway-app-connection-project-token-created.png)
</Step>
</Steps>
</Tab>
</Tabs>
## Create a Railway 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 Railway Connection">
Click **+ Add Connection** and choose **Railway Connection** from the list of integrations.
![Select Railway Connection](/images/app-connections/railway/railway-app-connection-option.png)
</Step>
<Step title="Fill out the Railway Connection form">
Complete the form by providing:
- A descriptive name for the connection
- An optional description
- The type of token you created earlier
- The token value from the previous step
![Railway Connection Modal](/images/app-connections/railway/railway-app-connection-form.png)
</Step>
<Step title="Connection created">
After submitting the form, your **Railway Connection** will be successfully created and ready to use with your Infisical projects.
![Railway Connection Created](/images/app-connections/railway/railway-app-connection-generated.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a Railway Connection via API, send a request to the [Create Railway Connection](/api-reference/endpoints/app-connections/railway/create) endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/railway \
--header 'Content-Type: application/json' \
--data '{
"name": "my-railway-connection",
"method": "team-token",
"credentials": {
"apiToken": "[TEAM TOKEN]"
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-railway-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": "railway",
"method": "team-token",
"credentials": {}
}
}
```
</Tab>
</Tabs>

View File

@ -0,0 +1,171 @@
---
title: "Railway Sync"
description: "Learn how to configure a Railway Sync for Infisical."
---
**Prerequisites:**
- Create a [Railway Connection](/integrations/app-connections/railway)
<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 'Railway'">
![Select Railway](/images/secret-syncs/railway/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/railway/railway-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/railway/railway-sync-destination.png)
- **Railway Connection**: The Railway Connection to authenticate with.
- **Project**: The Railway project to sync secrets to.
- **Environment**: The Railway environment to sync secrets to.
- **Service**: The Service to sync secrets to.
- **If not provided**: Secrets will be synced as [shared variables](https://docs.railway.com/guides/variables#shared-variables) on Railway.
</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/railway/railway-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.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Railway when keys conflict.
- **Import Secrets (Prioritize Railway)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Railway over Infisical when keys conflict.
- **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 Railway Sync, then click **Next**.
![Configure Details](/images/secret-syncs/railway/railway-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 Railway Sync configuration, then click **Create Sync**.
![Review Configuration](/images/secret-syncs/railway/railway-sync-review.png)
</Step>
<Step title="Sync created">
If enabled, your Railway Sync will begin syncing your secrets to the destination endpoint.
![Sync Created](/images/secret-syncs/railway/railway-sync-created.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a **Railway Sync**, make an API request to the [Create Railway Sync](/api-reference/endpoints/secret-syncs/railway/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/railway \
--header 'Content-Type: application/json' \
--data '{
"name": "my-railway-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": "dev-project-id",
"projectName": "Development Project",
"environmentId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environmentName": "Development",
"serviceId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"serviceName": "my-railway-service",
}
}'
```
### Sample response
```bash Response
{
"secretSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-railway-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": "railway",
"name": "my-railway-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": "railway",
"destinationConfig": {
"projectId": "dev-project-id",
"projectName": "Development Project",
"environmentId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environmentName": "Development",
"serviceId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"serviceName": "my-railway-service",
}
}
}
```
</Tab>
</Tabs>

View File

@ -0,0 +1,138 @@
import { useMemo } from "react";
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 {
TRailwayProject,
useRailwayConnectionListProjects
} from "@app/hooks/api/appConnections/railway";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const RailwaySyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Railway }
>();
const connectionId = useWatch({ name: "connection.id", control });
const projectId = useWatch({ name: "destinationConfig.projectId", control });
const { data: projects = [], isPending: isProjectsLoading } = useRailwayConnectionListProjects(
connectionId,
{
enabled: Boolean(connectionId)
}
);
const environments = useMemo(() => {
return projects.find((p) => p.id === projectId)?.environments ?? [];
}, [projects, projectId]);
const services = useMemo(() => {
return projects.find((p) => p.id === projectId)?.services ?? [];
}, [projects, projectId]);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.environmentId", "");
setValue("destinationConfig.projectId", "");
setValue("destinationConfig.serviceId", "");
setValue("destinationConfig.projectName", "");
setValue("destinationConfig.environmentName", "");
setValue("destinationConfig.serviceName", "");
}}
/>
<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<TRailwayProject>;
onChange(v?.id ?? null);
setValue("destinationConfig.projectName", v?.name ?? "");
}}
options={projects}
placeholder="Select a project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.environmentId"
disabled={!connectionId || !projectId}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Select an environment"
tooltipClassName="max-w-md"
>
<FilterableSelect
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={environments.find((p) => p.id === value) ?? null}
onChange={(option) => {
const v = option as SingleValue<TRailwayProject>;
onChange(v?.id ?? null);
setValue("destinationConfig.environmentName", v?.name ?? "");
}}
options={environments}
placeholder="Select an environment..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.serviceId"
disabled={!connectionId || !projectId}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Select a service"
tooltipClassName="max-w-md"
tooltipText="By default secrets are created as shared variables in Railway."
helperText="Scope your secrets to a specific service within the environment."
>
<FilterableSelect
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={services.find((p) => p.id === value) ?? null}
onChange={(option) => {
const v = option as SingleValue<TRailwayProject>;
onChange(v?.id ?? null);
setValue("destinationConfig.serviceName", v?.name ?? "");
}}
options={services}
placeholder="Select a service..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
</>
);
};

View File

@ -20,6 +20,7 @@ import { HCVaultSyncFields } from "./HCVaultSyncFields";
import { HerokuSyncFields } from "./HerokuSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { OCIVaultSyncFields } from "./OCIVaultSyncFields";
import { RailwaySyncFields } from "./RailwaySyncFields";
import { RenderSyncFields } from "./RenderSyncFields";
import { TeamCitySyncFields } from "./TeamCitySyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
@ -79,6 +80,8 @@ export const SecretSyncDestinationFields = () => {
return <CloudflarePagesSyncFields />;
case SecretSync.Zabbix:
return <ZabbixSyncFields />;
case SecretSync.Railway:
return <RailwaySyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}

View File

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

View File

@ -0,0 +1,29 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { useRailwayConnectionListProjects } from "@app/hooks/api/appConnections/railway";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const RailwaySyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Railway }>();
const connectionId = watch("connection.id");
const projectId = watch("destinationConfig.projectId");
const environmentId = watch("destinationConfig.environmentId");
const { data: projects = [] } = useRailwayConnectionListProjects(connectionId, {
enabled: Boolean(connectionId)
});
const project = projects.find((p) => p.id === projectId);
const environment = project?.environments.find((e) => e.id === environmentId);
return (
<>
<GenericFieldLabel label="Project">{project?.name ?? projectId}</GenericFieldLabel>
<GenericFieldLabel label="Environment">
{environment?.name ?? environmentId}
</GenericFieldLabel>
</>
);
};

View File

@ -30,6 +30,7 @@ import { HerokuSyncReviewFields } from "./HerokuSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
@ -128,6 +129,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Zabbix:
DestinationFieldsComponent = <ZabbixSyncReviewFields />;
break;
case SecretSync.Railway:
DestinationFieldsComponent = <RailwaySyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

@ -0,0 +1,18 @@
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 RailwaySyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Railway),
destinationConfig: z.object({
projectId: z.string().min(1, "Project ID is required"),
projectName: z.string(),
environmentName: z.string(),
environmentId: z.string().min(1, "Environment is required"),
serviceId: z.string().optional(),
serviceName: z.string().optional()
})
})
);

View File

@ -17,6 +17,7 @@ import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema
import { HerokuSyncDestinationSchema } from "./heroku-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema";
import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema";
import { RenderSyncDestinationSchema } from "./render-sync-destination-schema";
import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema";
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
@ -47,7 +48,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
FlyioSyncDestinationSchema,
GitlabSyncDestinationSchema,
CloudflarePagesSyncDestinationSchema,
ZabbixSyncDestinationSchema
ZabbixSyncDestinationSchema,
RailwaySyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;

View File

@ -43,6 +43,7 @@ import {
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
export const APP_CONNECTION_MAP: Record<
@ -91,8 +92,9 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" }
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@ -146,6 +148,12 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Simple Bind", icon: faLink };
case HerokuConnectionMethod.AuthToken:
return { name: "Auth Token", icon: faKey };
case RailwayConnectionMethod.AccountToken:
return { name: "Account Token", icon: faKey };
case RailwayConnectionMethod.TeamToken:
return { name: "Team Token", icon: faKey };
case RailwayConnectionMethod.ProjectToken:
return { name: "Project Token", icon: faKey };
case RenderConnectionMethod.ApiKey:
return { name: "API Key", icon: faKey };
default:

View File

@ -85,6 +85,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.Zabbix]: {
name: "Zabbix",
image: "Zabbix.png"
},
[SecretSync.Railway]: {
name: "Railway",
image: "Railway.png"
}
};
@ -111,7 +115,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.Gitlab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.Zabbix]: AppConnection.Zabbix
[SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<

View File

@ -29,5 +29,6 @@ export enum AppConnection {
Gitlab = "gitlab",
Cloudflare = "cloudflare",
Bitbucket = "bitbucket",
Zabbix = "zabbix"
Zabbix = "zabbix",
Railway = "railway"
}

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 { TRailwayProject } from "./types";
const railwayConnectionKeys = {
all: [...appConnectionKeys.all, "railway"] as const,
listSecretScopes: (connectionId: string) =>
[...railwayConnectionKeys.all, "workspace-scopes", connectionId] as const
};
export const useRailwayConnectionListProjects = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TRailwayProject[],
unknown,
TRailwayProject[],
ReturnType<typeof railwayConnectionKeys.listSecretScopes>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: railwayConnectionKeys.listSecretScopes(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ projects: TRailwayProject[] }>(
`/api/v1/app-connections/railway/${connectionId}/projects`
);
return data.projects;
},
...options
});
};

View File

@ -0,0 +1,12 @@
export type TRailwayProject = {
id: string;
name: string;
environments: Array<{
id: string;
name: string;
}>;
services: Array<{
id: string;
name: string;
}>;
};

View File

@ -140,6 +140,10 @@ export type TZabbixConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Zabbix;
};
export type TRailwayConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Railway;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@ -169,7 +173,8 @@ export type TAppConnectionOption =
| TGitlabConnectionOption
| TCloudflareConnectionOption
| TBitbucketConnectionOption
| TZabbixConnectionOption;
| TZabbixConnectionOption
| TRailwayConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@ -203,4 +208,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
[AppConnection.Zabbix]: TZabbixConnectionOption;
[AppConnection.Railway]: TRailwayConnectionOption;
};

View File

@ -25,6 +25,7 @@ import { TMySqlConnection } from "./mysql-connection";
import { TOCIConnection } from "./oci-connection";
import { TOracleDBConnection } from "./oracledb-connection";
import { TPostgresConnection } from "./postgres-connection";
import { TRailwayConnection } from "./railway-connection";
import { TRenderConnection } from "./render-connection";
import { TTeamCityConnection } from "./teamcity-connection";
import { TTerraformCloudConnection } from "./terraform-cloud-connection";
@ -95,7 +96,8 @@ export type TAppConnection =
| TGitLabConnection
| TCloudflareConnection
| TBitbucketConnection
| TZabbixConnection;
| TZabbixConnection
| TRailwayConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@ -154,4 +156,5 @@ export type TAppConnectionMap = {
[AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Bitbucket]: TBitbucketConnection;
[AppConnection.Zabbix]: TZabbixConnection;
[AppConnection.Railway]: TRailwayConnection;
};

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 RailwayConnectionMethod {
AccountToken = "account-token",
ProjectToken = "project-token",
TeamToken = "team-token"
}
export type TRailwayConnection = TRootAppConnection & { app: AppConnection.Railway } & {
method: RailwayConnectionMethod;
credentials: {
apiToken: string;
};
};

View File

@ -21,7 +21,8 @@ export enum SecretSync {
Flyio = "flyio",
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages",
Zabbix = "zabbix"
Zabbix = "zabbix",
Railway = "railway"
}
export enum SecretSyncStatus {

View File

@ -19,6 +19,7 @@ import { THCVaultSync } from "./hc-vault-sync";
import { THerokuSync } from "./heroku-sync";
import { THumanitecSync } from "./humanitec-sync";
import { TOCIVaultSync } from "./oci-vault-sync";
import { TRailwaySync } from "./railway-sync";
import { TTeamCitySync } from "./teamcity-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync";
import { TVercelSync } from "./vercel-sync";
@ -55,7 +56,8 @@ export type TSecretSync =
| TFlyioSync
| TGitLabSync
| TCloudflarePagesSync
| TZabbixSync;
| TZabbixSync
| TRailwaySync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@ -0,0 +1,22 @@
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 TRailwaySync = TRootSecretSync & {
destination: SecretSync.Railway;
destinationConfig: {
projectId: string;
projectName: string;
environmentName: string;
environmentId: string;
serviceId?: string;
serviceName?: string;
};
connection: {
app: AppConnection.Railway;
name: string;
id: string;
};
};

View File

@ -34,6 +34,7 @@ import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { RailwayConnectionForm } from "./RailwayConnectionForm";
import { RenderConnectionForm } from "./RenderConnectionForm";
import { TeamCityConnectionForm } from "./TeamCityConnectionForm";
import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
@ -140,6 +141,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <BitbucketConnectionForm onSubmit={onSubmit} />;
case AppConnection.Zabbix:
return <ZabbixConnectionForm onSubmit={onSubmit} />;
case AppConnection.Railway:
return <RailwayConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@ -238,6 +241,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Zabbix:
return <ZabbixConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Railway:
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}

View File

@ -0,0 +1,135 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
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 {
RailwayConnectionMethod,
TRailwayConnection
} from "@app/hooks/api/appConnections/types/railway-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TRailwayConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Railway)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.nativeEnum(RailwayConnectionMethod),
credentials: z.object({
apiToken: z.string().trim().min(1, "Service API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const RailwayConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Railway,
method: RailwayConnectionMethod.AccountToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<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.Railway].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(RailwayConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Token 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 Railway"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@ -0,0 +1,14 @@
import { TRailwaySync } from "@app/hooks/api/secretSyncs/types/railway-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TRailwaySync;
};
export const RailwaySyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@ -17,6 +17,7 @@ import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol";
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
@ -76,6 +77,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <CloudflarePagesSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Zabbix:
return <ZabbixSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Railway:
return <RailwaySyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

View File

@ -156,6 +156,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
throw new Error(`Unhandled Zabbix Scope Destination Col Values ${destination}`);
}
break;
case SecretSync.Railway:
primaryText = "Railway Project";
secondaryText = destinationConfig.projectName;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}

View File

@ -0,0 +1,17 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TRailwaySync } from "@app/hooks/api/secretSyncs/types/railway-sync";
type Props = {
secretSync: TRailwaySync;
};
export const RailwaySyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
return (
<>
<GenericFieldLabel label="Project">{destinationConfig.projectName}</GenericFieldLabel>
<GenericFieldLabel label="Environment">{destinationConfig.environmentName}</GenericFieldLabel>
</>
);
};

View File

@ -28,6 +28,7 @@ import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection";
import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection";
import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection";
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
@ -118,6 +119,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Zabbix:
DestinationComponents = <ZabbixSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Railway:
DestinationComponents = <RailwaySyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}

View File

@ -61,6 +61,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
case SecretSync.Zabbix:
case SecretSync.Railway:
AdditionalSyncOptionsComponent = null;
break;
default: