feat: adds secret sync handlers

This commit is contained in:
Piyush Gupta
2025-10-13 17:16:02 +05:30
parent 5b765841b7
commit 789d64843e
20 changed files with 289 additions and 46 deletions
+4 -1
View File
@@ -2512,8 +2512,11 @@ export const SecretSyncs = {
},
LARAVEL_FORGE: {
orgSlug: "The slug of the Laravel Forge org to sync secrets to.",
orgName: "The name of the Laravel Forge org to sync secrets to.",
serverId: "The ID of the Laravel Forge server to sync secrets to.",
siteId: "The ID of the Laravel Forge site to sync secrets to."
serverName: "The name of the Laravel Forge server to sync secrets to.",
siteId: "The ID of the Laravel Forge site to sync secrets to.",
siteName: "The name of the Laravel Forge site to sync secrets to."
},
WINDMILL: {
workspace: "The Windmill workspace to sync secrets to.",
@@ -1,3 +1,4 @@
export * from "./laravel-forge-sync-constants";
export * from "./laravel-forge-sync-fns";
export * from "./laravel-forge-sync-schemas";
export * from "./laravel-forge-sync-types";
@@ -0,0 +1,129 @@
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import {
LaravelForgeSecret,
TGetLaravelForgeSecrets,
TLaravelForgeSecrets,
TLaravelForgeSyncWithCredentials
} from "./laravel-forge-sync-types";
const getLaravelForgeSecretsRaw = async ({ apiToken, orgSlug, serverId, siteId }: TGetLaravelForgeSecrets) => {
const { data } = await request.get<TLaravelForgeSecrets>(
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${orgSlug}/servers/${serverId}/sites/${siteId}/environment`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json",
"Content-Type": "application/json"
}
}
);
return data.data.attributes.content;
};
const parseEnv = (str: string) => {
const lines = str.split("\n");
const parsed: { key: string; value: string }[] = [];
lines.forEach((line) => {
const trimmed = line.trim();
const isInvalidLine = trimmed === "" || trimmed.startsWith("#");
if (!isInvalidLine && trimmed.includes("=")) {
const equalIndex = trimmed.indexOf("=");
const key = trimmed.substring(0, equalIndex).trim();
const valueRaw = trimmed.substring(equalIndex + 1).trim();
let value = valueRaw;
if ((value.startsWith(`"`) && value.endsWith(`"`)) || (value.startsWith(`'`) && value.endsWith(`'`))) {
value = value.slice(1, -1);
}
parsed.push({
key,
value
});
}
});
return parsed;
};
const getLaravelForgeSecrets = async (secretSync: TLaravelForgeSyncWithCredentials): Promise<LaravelForgeSecret[]> => {
const {
connection,
destinationConfig: { orgSlug, serverId, siteId }
} = secretSync;
const { apiToken } = connection.credentials;
const secrets = await getLaravelForgeSecretsRaw({ apiToken, orgSlug, serverId, siteId });
const parsedSecrets = parseEnv(secrets);
return parsedSecrets;
};
const buildEnvString = (secrets: LaravelForgeSecret[]) => {
return secrets.map((secret) => `${secret.key}=${secret.value}`).join("\n");
};
const updateLaravelForgeSecrets = async (secretSync: TLaravelForgeSyncWithCredentials, envString: string) => {
const {
connection,
destinationConfig: { orgSlug, serverId, siteId }
} = secretSync;
const { apiToken } = connection.credentials;
await request.put(
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${orgSlug}/servers/${serverId}/sites/${siteId}/environment`,
{
environment: envString
},
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json",
"Content-Type": "application/json"
}
}
);
};
export const LaravelForgeSyncFns = {
async syncSecrets(secretSync: TLaravelForgeSyncWithCredentials, secretMap: TSecretMap) {
const secrets = await getLaravelForgeSecrets(secretSync);
const secretsMap = Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
for (const [key, { value }] of Object.entries(secretMap)) {
secretsMap[key] = { value };
}
await updateLaravelForgeSecrets(secretSync, buildEnvString(secrets));
},
async getSecrets(secretSync: TLaravelForgeSyncWithCredentials): Promise<TSecretMap> {
const secrets = await getLaravelForgeSecrets(secretSync);
return Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
},
async removeSecrets(secretSync: TLaravelForgeSyncWithCredentials, secretMap: TSecretMap) {
const existingSecrets = await getLaravelForgeSecrets(secretSync);
const newSecrets = existingSecrets.filter((secret) => !Object.hasOwn(secretMap, secret.key));
if (newSecrets.length === existingSecrets.length) {
return;
}
const envString = buildEnvString(newSecrets);
await updateLaravelForgeSecrets(secretSync, envString);
}
};
@@ -12,8 +12,14 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"
const LaravelForgeSyncDestinationConfigSchema = z.object({
orgSlug: z.string().min(1, "Org Slug is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.orgSlug),
orgName: z.string().min(1, "Org Name is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.orgName),
serverId: z.string().min(1, "Server ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.serverId),
siteId: z.string().min(1, "Site ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteId)
serverName: z
.string()
.min(1, "Server Name is required")
.describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.serverName),
siteId: z.string().min(1, "Site ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteId),
siteName: z.string().min(1, "Site Name is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteName)
});
const LaravelForgeSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
@@ -18,24 +18,29 @@ export type TLaravelForgeSyncWithCredentials = TLaravelForgeSync & {
connection: TLaravelForgeConnection;
};
export type TGetLaravelForgeSecrets = {
apiToken: string;
orgSlug: string;
serverId: string;
siteId: string;
};
export type TLaravelForgeSecrets = {
data: {
id: string;
type: string;
attributes: {
content: string;
};
};
};
export type LaravelForgeSecret = {
description: string;
is_secret: boolean;
key: string;
source: "app" | "env";
value: string;
};
export interface LaravelForgeApiSecret {
id: string;
key: string;
value: string;
type: string;
target: string[];
customEnvironmentIds?: string[];
gitBranch?: string;
createdAt?: number;
updatedAt?: number;
configurationId?: string;
system?: boolean;
}
@@ -50,6 +50,7 @@ import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge";
import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns";
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
import { RailwaySyncFns } from "./railway/railway-sync-fns";
@@ -279,6 +280,8 @@ export const SecretSyncFns = {
return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket:
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.LaravelForge:
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -395,6 +398,9 @@ export const SecretSyncFns = {
case SecretSync.Bitbucket:
secretMap = await BitbucketSyncFns.getSecrets(secretSync);
break;
case SecretSync.LaravelForge:
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -488,6 +494,8 @@ export const SecretSyncFns = {
return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket:
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.LaravelForge:
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`