Files
infisical/backend/src/services/secret-sync/laravel-forge/laravel-forge-sync-fns.ts
T
2025-10-15 21:27:44 +05:30

208 lines
6.0 KiB
TypeScript

import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
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 }[] = [];
let i = 0;
while (i < lines.length) {
const trimmed = lines[i].trim();
// Skip empty lines and comments
if (trimmed === "" || trimmed.startsWith("#")) {
i += 1;
// eslint-disable-next-line no-continue
continue;
}
if (trimmed.includes("=")) {
const equalIndex = trimmed.indexOf("=");
const key = trimmed.substring(0, equalIndex).trim();
const valueRaw = trimmed.substring(equalIndex + 1).trim();
// Check if value starts with a quote
const startsWithDoubleQuote = valueRaw.startsWith('"');
const startsWithSingleQuote = valueRaw.startsWith("'");
if (startsWithDoubleQuote || startsWithSingleQuote) {
const quoteChar = startsWithDoubleQuote ? '"' : "'";
const closingQuoteIndex = valueRaw.indexOf(quoteChar, 1);
if (closingQuoteIndex !== -1) {
// Single-line quoted value
const value = valueRaw.slice(1, closingQuoteIndex);
parsed.push({ key, value });
i += 1;
} else {
// Multiline quoted value - collect lines until closing quote
let value = valueRaw.slice(1);
i += 1;
while (i < lines.length) {
const nextLine = lines[i];
const closingIndex = nextLine.indexOf(quoteChar);
if (closingIndex !== -1) {
value += `\n${nextLine.substring(0, closingIndex)}`;
parsed.push({ key, value });
i += 1;
break;
} else {
value += `\n${nextLine}`;
i += 1;
}
}
}
} else {
// Unquoted value
parsed.push({ key, value: valueRaw });
i += 1;
}
} else {
i += 1;
}
}
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[]) => {
if (secrets.length === 0) {
return "# .env";
}
return secrets
.map((secret) => {
const { value } = secret;
if (value.includes(`"`)) {
return `${secret.key}='${value}'`;
}
if (value.includes(" ") || value.includes("\n") || value.includes(`'`)) {
return `${secret.key}="${value}"`;
}
return `${secret.key}=${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 {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const secrets = await getLaravelForgeSecrets(secretSync);
// Create a map of the existing secrets
const updatedSecretsMap = new Map(secrets.map((secret) => [secret.key, secret.value]));
for (const [key, { value }] of Object.entries(secretMap)) {
// Add the new secrets to the map
updatedSecretsMap.set(key, value);
}
if (!disableSecretDeletion) {
secrets.forEach((secret) => {
if (!matchesSchema(secret.key, environment?.slug || "", keySchema)) return;
if (!secretMap[secret.key]) {
updatedSecretsMap.delete(secret.key);
}
});
}
const updatedSecrets = Array.from(updatedSecretsMap.entries()).map(([key, value]) => ({ key, value }));
const envString = buildEnvString(updatedSecrets);
await updateLaravelForgeSecrets(secretSync, envString);
},
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);
}
};