Merge branch 'heads/main' into daniel/fips-initative

This commit is contained in:
Daniel Hougaard
2025-07-11 13:11:07 +04:00
153 changed files with 3313 additions and 355 deletions
@@ -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
};
@@ -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
});
}
}
}
};
@@ -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)
});
@@ -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>;
};
};
@@ -21,7 +21,8 @@ export enum SecretSync {
Flyio = "flyio",
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages",
Zabbix = "zabbix"
Zabbix = "zabbix",
Railway = "railway"
}
export enum SecretSyncInitialSyncBehavior {
@@ -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}`
@@ -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
};
@@ -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;