mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: Add Netlify app connection and secrets sync (#4205)
* fix: save wip * feat: final impl * feat: docs * Update backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * chore: remove empty conflict files * Update backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: lint * feat: Netlify app connection and secrets sync * feat: docs * fix: type check * fix: api client * fix: lint and types * fix: typecheck lint * fix: docs * fix: lint * fix: lint * fix: PR changes * fix: typecheck * fix: PR changes * fix PR changes * fix: PR Change * fix: type error * Small tweaks * fix: support is_secret * fix: revert is_secret * fix: force update existing netlify secret --------- Co-authored-by: sidwebworks <xodeveloper@gmail.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: x032205 <x032205@gmail.com>
This commit is contained in:
@@ -2302,6 +2302,9 @@ export const AppConnections = {
|
||||
DIGITAL_OCEAN_APP_PLATFORM: {
|
||||
apiToken: "The API token used to authenticate with Digital Ocean App Platform."
|
||||
},
|
||||
NETLIFY: {
|
||||
accessToken: "The Access token used to authenticate with Netlify."
|
||||
},
|
||||
OKTA: {
|
||||
instanceUrl: "The URL used to access your Okta organization.",
|
||||
apiToken: "The API token used to authenticate with Okta."
|
||||
@@ -2533,6 +2536,13 @@ export const SecretSyncs = {
|
||||
workspaceSlug: "The Bitbucket Workspace slug to sync secrets to.",
|
||||
repositorySlug: "The Bitbucket Repository slug to sync secrets to.",
|
||||
environmentId: "The Bitbucket Deployment Environment uuid to sync secrets to."
|
||||
},
|
||||
NETLIFY: {
|
||||
accountId: "The ID of the Netlify account to sync secrets to.",
|
||||
accountName: "The name of the Netlify account to sync secrets to.",
|
||||
siteName: "The name of the Netlify site to sync secrets to.",
|
||||
siteId: "The ID of the Netlify site to sync secrets to.",
|
||||
context: "The Netlify context to sync secrets to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,6 +75,10 @@ import {
|
||||
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
|
||||
import {
|
||||
NetlifyConnectionListItemSchema,
|
||||
SanitizedNetlifyConnectionSchema
|
||||
} from "@app/services/app-connection/netlify";
|
||||
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
|
||||
import {
|
||||
PostgresConnectionListItemSchema,
|
||||
@@ -145,6 +149,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedChecklyConnectionSchema.options,
|
||||
...SanitizedSupabaseConnectionSchema.options,
|
||||
...SanitizedDigitalOceanConnectionSchema.options,
|
||||
...SanitizedNetlifyConnectionSchema.options,
|
||||
...SanitizedOktaConnectionSchema.options
|
||||
]);
|
||||
|
||||
@@ -184,6 +189,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
ChecklyConnectionListItemSchema,
|
||||
SupabaseConnectionListItemSchema,
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
NetlifyConnectionListItemSchema,
|
||||
OktaConnectionListItemSchema
|
||||
]);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router
|
||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
|
||||
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
|
||||
import { registerOktaConnectionRouter } from "./okta-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerRailwayConnectionRouter } from "./railway-connection-router";
|
||||
@@ -76,5 +77,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Checkly]: registerChecklyConnectionRouter,
|
||||
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
|
||||
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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 {
|
||||
CreateNetlifyConnectionSchema,
|
||||
SanitizedNetlifyConnectionSchema,
|
||||
UpdateNetlifyConnectionSchema
|
||||
} from "@app/services/app-connection/netlify";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerNetlifyConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Netlify,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedNetlifyConnectionSchema,
|
||||
createSchema: CreateNetlifyConnectionSchema,
|
||||
updateSchema: UpdateNetlifyConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/accounts`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accounts: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const accounts = await server.services.appConnection.netlify.listAccounts(connectionId, req.permission);
|
||||
|
||||
return { accounts };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/accounts/:accountId/sites`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid(),
|
||||
accountId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
sites: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId, accountId } = req.params;
|
||||
|
||||
const sites = await server.services.appConnection.netlify.listSites(connectionId, req.permission, accountId);
|
||||
|
||||
return { sites };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -21,6 +21,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 { registerNetlifySyncRouter } from "./netlify-sync-router";
|
||||
import { registerRailwaySyncRouter } from "./railway-sync-router";
|
||||
import { registerRenderSyncRouter } from "./render-sync-router";
|
||||
import { registerSupabaseSyncRouter } from "./supabase-sync-router";
|
||||
@@ -61,5 +62,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Railway]: registerRailwaySyncRouter,
|
||||
[SecretSync.Checkly]: registerChecklySyncRouter,
|
||||
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
|
||||
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreateNetlifySyncSchema,
|
||||
NetlifySyncSchema,
|
||||
UpdateNetlifySyncSchema
|
||||
} from "@app/services/secret-sync/netlify/netlify-sync-schemas";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerNetlifySyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Netlify,
|
||||
server,
|
||||
responseSchema: NetlifySyncSchema,
|
||||
createSchema: CreateNetlifySyncSchema,
|
||||
updateSchema: UpdateNetlifySyncSchema
|
||||
});
|
||||
@@ -44,6 +44,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 { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify";
|
||||
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 { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
|
||||
@@ -82,6 +83,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
RailwaySyncSchema,
|
||||
ChecklySyncSchema,
|
||||
DigitalOceanAppPlatformSyncSchema,
|
||||
NetlifySyncSchema,
|
||||
BitbucketSyncSchema
|
||||
]);
|
||||
|
||||
@@ -114,6 +116,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
RailwaySyncListItemSchema,
|
||||
ChecklySyncListItemSchema,
|
||||
SupabaseSyncListItemSchema,
|
||||
NetlifySyncListItemSchema,
|
||||
BitbucketSyncListItemSchema
|
||||
]);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export enum AppConnection {
|
||||
Checkly = "checkly",
|
||||
Supabase = "supabase",
|
||||
DigitalOcean = "digital-ocean",
|
||||
Netlify = "netlify",
|
||||
Okta = "okta"
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnection
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
|
||||
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
|
||||
import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify";
|
||||
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
|
||||
@@ -163,6 +164,7 @@ export const listAppConnectionOptions = () => {
|
||||
getChecklyConnectionListItem(),
|
||||
getSupabaseConnectionListItem(),
|
||||
getDigitalOceanConnectionListItem(),
|
||||
getNetlifyConnectionListItem(),
|
||||
getOktaConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
@@ -251,7 +253,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
|
||||
@@ -381,6 +384,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Checkly]: "Checkly",
|
||||
[AppConnection.Supabase]: "Supabase",
|
||||
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
|
||||
[AppConnection.Netlify]: "Netlify",
|
||||
[AppConnection.Okta]: "Okta"
|
||||
};
|
||||
|
||||
@@ -75,5 +76,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -81,6 +81,8 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
|
||||
import { netlifyConnectionService } from "./netlify/netlify-connection-service";
|
||||
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
|
||||
import { oktaConnectionService } from "./okta/okta-connection-service";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
@@ -148,6 +150,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
|
||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
@@ -611,6 +614,7 @@ export const appConnectionServiceFactory = ({
|
||||
checkly: checklyConnectionService(connectAppConnectionById),
|
||||
supabase: supabaseConnectionService(connectAppConnectionById),
|
||||
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
|
||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -149,6 +149,12 @@ import {
|
||||
} from "./ldap";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import {
|
||||
TNetlifyConnection,
|
||||
TNetlifyConnectionConfig,
|
||||
TNetlifyConnectionInput,
|
||||
TValidateNetlifyConnectionCredentialsSchema
|
||||
} from "./netlify";
|
||||
import {
|
||||
TOktaConnection,
|
||||
TOktaConnectionConfig,
|
||||
@@ -245,6 +251,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TChecklyConnection
|
||||
| TSupabaseConnection
|
||||
| TDigitalOceanConnection
|
||||
| TNetlifyConnection
|
||||
| TOktaConnection
|
||||
);
|
||||
|
||||
@@ -288,6 +295,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TChecklyConnectionInput
|
||||
| TSupabaseConnectionInput
|
||||
| TDigitalOceanConnectionInput
|
||||
| TNetlifyConnectionInput
|
||||
| TOktaConnectionInput
|
||||
);
|
||||
|
||||
@@ -339,6 +347,7 @@ export type TAppConnectionConfig =
|
||||
| TChecklyConnectionConfig
|
||||
| TSupabaseConnectionConfig
|
||||
| TDigitalOceanConnectionConfig
|
||||
| TNetlifyConnectionConfig
|
||||
| TOktaConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
@@ -377,6 +386,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
| TValidateSupabaseConnectionCredentialsSchema
|
||||
| TValidateDigitalOceanCredentialsSchema
|
||||
| TValidateNetlifyConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
|
||||
4
backend/src/services/app-connection/netlify/index.ts
Normal file
4
backend/src/services/app-connection/netlify/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./netlify-connection-constants";
|
||||
export * from "./netlify-connection-fns";
|
||||
export * from "./netlify-connection-schemas";
|
||||
export * from "./netlify-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum NetlifyConnectionMethod {
|
||||
AccessToken = "access-token"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* 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 { NetlifyConnectionMethod } from "./netlify-connection-constants";
|
||||
import { NetlifyPublicAPI } from "./netlify-connection-public-client";
|
||||
import { TNetlifyConnectionConfig } from "./netlify-connection-types";
|
||||
|
||||
export const getNetlifyConnectionListItem = () => {
|
||||
return {
|
||||
name: "Netlify" as const,
|
||||
app: AppConnection.Netlify as const,
|
||||
methods: Object.values(NetlifyConnectionMethod)
|
||||
};
|
||||
};
|
||||
|
||||
export const validateNetlifyConnectionCredentials = async (config: TNetlifyConnectionConfig) => {
|
||||
try {
|
||||
await NetlifyPublicAPI.healthcheck(config);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection - verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxiosError } from "axios";
|
||||
|
||||
import { createRequestClient } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { NetlifyConnectionMethod } from "./netlify-connection-constants";
|
||||
import { TNetlifyAccount, TNetlifyConnectionConfig, TNetlifySite, TNetlifyVariable } from "./netlify-connection-types";
|
||||
|
||||
export function getNetlifyAuthHeaders(connection: TNetlifyConnectionConfig): Record<string, string> {
|
||||
switch (connection.method) {
|
||||
case NetlifyConnectionMethod.AccessToken:
|
||||
return {
|
||||
Authorization: `Bearer ${connection.credentials.accessToken}`
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported Netlify connection method`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getNetlifyRatelimiter(response: AxiosResponse): {
|
||||
maxAttempts: number;
|
||||
isRatelimited: boolean;
|
||||
wait: () => Promise<void>;
|
||||
} {
|
||||
const wait = (seconds: number = 60) => {
|
||||
return new Promise<void>((res) => {
|
||||
setTimeout(res, seconds * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
let remaining = parseInt(response.headers["X-RateLimit-Remaining"] as string, 10);
|
||||
let isRatelimited = response.status === HttpStatusCode.TooManyRequests;
|
||||
|
||||
if (isRatelimited) {
|
||||
if (Math.round(remaining) > 0) {
|
||||
isRatelimited = true;
|
||||
remaining += 1; // Jitter to ensure we wait at least 1 second
|
||||
} else {
|
||||
remaining = 60;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isRatelimited,
|
||||
wait: () => wait(remaining),
|
||||
maxAttempts: 3
|
||||
};
|
||||
}
|
||||
|
||||
type NetlifyParams = {
|
||||
account_id: string;
|
||||
context_name?: string;
|
||||
site_id?: string;
|
||||
};
|
||||
|
||||
class NetlifyPublicClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = createRequestClient({
|
||||
baseURL: `${IntegrationUrls.NETLIFY_API_URL}/api/v1`,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async send<T>(connection: TNetlifyConnectionConfig, config: AxiosRequestConfig, retryAttempt = 0): Promise<T> {
|
||||
const response = await this.client.request<T>({
|
||||
...config,
|
||||
timeout: 1000 * 60, // 60 seconds timeout
|
||||
validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests,
|
||||
headers: getNetlifyAuthHeaders(connection)
|
||||
});
|
||||
const limiter = getNetlifyRatelimiter(response);
|
||||
|
||||
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
|
||||
await limiter.wait();
|
||||
return this.send(connection, config, retryAttempt + 1);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
healthcheck(connection: TNetlifyConnectionConfig) {
|
||||
switch (connection.method) {
|
||||
case NetlifyConnectionMethod.AccessToken:
|
||||
return this.getNetlifyAccounts(connection);
|
||||
default:
|
||||
throw new Error(`Unsupported Netlify connection method`);
|
||||
}
|
||||
}
|
||||
|
||||
async getVariables(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
limit: number = 50,
|
||||
page: number = 1
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable[]>(connection, {
|
||||
method: "GET",
|
||||
url: `/accounts/${account_id}/env`,
|
||||
params: {
|
||||
...params,
|
||||
limit,
|
||||
page
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async createVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
...variables: TNetlifyVariable[]
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "POST",
|
||||
url: `/accounts/${account_id}/env`,
|
||||
data: variables,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateVariableValue(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: TNetlifyVariable
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "PATCH",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
data: variable,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: TNetlifyVariable
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "PUT",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
data: variable,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: Pick<TNetlifyVariable, "key">
|
||||
) {
|
||||
try {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "GET",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async upsertVariable(connection: TNetlifyConnectionConfig, params: NetlifyParams, variable: TNetlifyVariable) {
|
||||
const res = await this.getVariable(connection, params, variable);
|
||||
|
||||
if (!res) {
|
||||
return this.createVariable(connection, params, variable);
|
||||
}
|
||||
|
||||
if (res.is_secret) {
|
||||
await this.deleteVariable(connection, params, variable);
|
||||
return this.createVariable(connection, params, variable);
|
||||
}
|
||||
|
||||
return this.updateVariable(connection, params, variable);
|
||||
}
|
||||
|
||||
async deleteVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: Pick<TNetlifyVariable, "key">
|
||||
) {
|
||||
try {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "DELETE",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteVariableValue(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, value_id, ...params }: NetlifyParams & { value_id: string },
|
||||
variable: Pick<TNetlifyVariable, "key" | "id">
|
||||
) {
|
||||
try {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "DELETE",
|
||||
url: `/accounts/${account_id}/${variable.key}/value/${value_id}`,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getSites(connection: TNetlifyConnectionConfig, accountId: string) {
|
||||
const res = await this.send<TNetlifySite[]>(connection, {
|
||||
method: "GET",
|
||||
url: `/${accountId}/sites`
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getNetlifyAccounts(connection: TNetlifyConnectionConfig) {
|
||||
const res = await this.send<TNetlifyAccount[]>(connection, {
|
||||
method: "GET",
|
||||
url: `/accounts`
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export const NetlifyPublicAPI = new NetlifyPublicClient();
|
||||
@@ -0,0 +1,67 @@
|
||||
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 { NetlifyConnectionMethod } from "./netlify-connection-constants";
|
||||
|
||||
export const NetlifyConnectionMethodSchema = z
|
||||
.nativeEnum(NetlifyConnectionMethod)
|
||||
.describe(AppConnections.CREATE(AppConnection.Netlify).method);
|
||||
|
||||
export const NetlifyConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.NETLIFY.accessToken)
|
||||
});
|
||||
|
||||
const BaseNetlifyConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.Netlify)
|
||||
});
|
||||
|
||||
export const NetlifyConnectionSchema = BaseNetlifyConnectionSchema.extend({
|
||||
method: NetlifyConnectionMethodSchema,
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedNetlifyConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseNetlifyConnectionSchema.extend({
|
||||
method: NetlifyConnectionMethodSchema,
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema.pick({})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateNetlifyConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: NetlifyConnectionMethodSchema,
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Netlify).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateNetlifyConnectionSchema = ValidateNetlifyConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Netlify)
|
||||
);
|
||||
|
||||
export const UpdateNetlifyConnectionSchema = z
|
||||
.object({
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Netlify).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Netlify));
|
||||
|
||||
export const NetlifyConnectionListItemSchema = z.object({
|
||||
name: z.literal("Netlify"),
|
||||
app: z.literal(AppConnection.Netlify),
|
||||
methods: z.nativeEnum(NetlifyConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { NetlifyPublicAPI } from "./netlify-connection-public-client";
|
||||
import { TNetlifyConnection } from "./netlify-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TNetlifyConnection>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const netlifyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listAccounts = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Netlify, connectionId, actor);
|
||||
try {
|
||||
const accounts = await NetlifyPublicAPI.getNetlifyAccounts(appConnection);
|
||||
return accounts;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list accounts on Netlify");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const listSites = async (connectionId: string, actor: OrgServiceActor, accountId: string) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Netlify, connectionId, actor);
|
||||
try {
|
||||
const sites = await NetlifyPublicAPI.getSites(appConnection, accountId);
|
||||
return sites;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list sites on Netlify");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listAccounts,
|
||||
listSites
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateNetlifyConnectionSchema,
|
||||
NetlifyConnectionSchema,
|
||||
ValidateNetlifyConnectionCredentialsSchema
|
||||
} from "./netlify-connection-schemas";
|
||||
|
||||
export type TNetlifyConnection = z.infer<typeof NetlifyConnectionSchema>;
|
||||
|
||||
export type TNetlifyConnectionInput = z.infer<typeof CreateNetlifyConnectionSchema> & {
|
||||
app: AppConnection.Netlify;
|
||||
};
|
||||
|
||||
export type TValidateNetlifyConnectionCredentialsSchema = typeof ValidateNetlifyConnectionCredentialsSchema;
|
||||
|
||||
export type TNetlifyConnectionConfig = DiscriminativePick<TNetlifyConnection, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TNetlifyVariable = {
|
||||
key: string;
|
||||
id?: string; // ID of the variable (present in responses)
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
is_secret?: boolean;
|
||||
scopes?: ("builds" | "functions" | "runtime" | "post_processing")[];
|
||||
values: TNetlifyVariableValue[];
|
||||
};
|
||||
|
||||
export type TNetlifyVariableValue = {
|
||||
id?: string;
|
||||
context?: string; // "all", "dev", "branch-deploy", etc.
|
||||
value?: string; // Omitted in response if `is_secret` is true
|
||||
site_id?: string; // Optional: overrides at site-level
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TNetlifyAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TNetlifySite = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
4
backend/src/services/secret-sync/netlify/index.ts
Normal file
4
backend/src/services/secret-sync/netlify/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./netlify-sync-constants";
|
||||
export * from "./netlify-sync-fns";
|
||||
export * from "./netlify-sync-schemas";
|
||||
export * from "./netlify-sync-types";
|
||||
@@ -0,0 +1,19 @@
|
||||
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 NETLIFY_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Netlify",
|
||||
destination: SecretSync.Netlify,
|
||||
connection: AppConnection.Netlify,
|
||||
canImportSecrets: true
|
||||
};
|
||||
|
||||
export enum NetlifySyncContext {
|
||||
All = "all",
|
||||
DeployPreview = "deploy-preview",
|
||||
Production = "production",
|
||||
BranchDeploy = "branch-deploy",
|
||||
Dev = "dev",
|
||||
Branch = "branch"
|
||||
}
|
||||
125
backend/src/services/secret-sync/netlify/netlify-sync-fns.ts
Normal file
125
backend/src/services/secret-sync/netlify/netlify-sync-fns.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/* eslint-disable no-continue */
|
||||
import { NetlifyPublicAPI } from "@app/services/app-connection/netlify/netlify-connection-public-client";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SecretSyncError } from "../secret-sync-errors";
|
||||
import type { TNetlifySyncWithCredentials } from "./netlify-sync-types";
|
||||
|
||||
export const NetlifySyncFns = {
|
||||
async getSecrets(secretSync: TNetlifySyncWithCredentials): Promise<TSecretMap> {
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const params = {
|
||||
account_id: config.accountId,
|
||||
context_name: config.context, // Only used in the case of getVariables
|
||||
site_id: config.siteId
|
||||
};
|
||||
|
||||
const variables = await NetlifyPublicAPI.getVariables(secretSync.connection, params);
|
||||
|
||||
const map = {} as TSecretMap;
|
||||
|
||||
for (const variable of variables) {
|
||||
if (variable.is_secret) continue;
|
||||
|
||||
const latest = variable.values.sort((a, b) => Number(b.created_at) - Number(a.created_at))[0];
|
||||
|
||||
map[variable.key] = {
|
||||
value: latest?.value ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
return map;
|
||||
},
|
||||
|
||||
async syncSecrets(secretSync: TNetlifySyncWithCredentials, secretMap: TSecretMap) {
|
||||
const {
|
||||
environment,
|
||||
syncOptions: { disableSecretDeletion, keySchema }
|
||||
} = secretSync;
|
||||
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const params = {
|
||||
account_id: config.accountId,
|
||||
context_name: config.context,
|
||||
site_id: config.siteId
|
||||
};
|
||||
|
||||
const variables = await NetlifyPublicAPI.getVariables(secretSync.connection, params);
|
||||
|
||||
const existing = Object.fromEntries(variables.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
const entry = secretMap[key];
|
||||
|
||||
await NetlifyPublicAPI.upsertVariable(secretSync.connection, params, {
|
||||
key,
|
||||
is_secret: false,
|
||||
values: [
|
||||
{
|
||||
value: entry.value,
|
||||
context: config.context
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
for await (const key of Object.keys(existing)) {
|
||||
try {
|
||||
// eslint-disable-next-line no-continue, @typescript-eslint/no-unsafe-argument
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!secretMap[key]) {
|
||||
await NetlifyPublicAPI.deleteVariable(secretSync.connection, params, {
|
||||
key
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async removeSecrets(secretSync: TNetlifySyncWithCredentials, secretMap: TSecretMap) {
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const params = {
|
||||
account_id: config.accountId,
|
||||
context_name: config.context,
|
||||
site_id: config.siteId
|
||||
};
|
||||
|
||||
const variables = await NetlifyPublicAPI.getVariables(secretSync.connection, params);
|
||||
|
||||
const existingSecrets = Object.fromEntries(variables.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const secret of Object.keys(existingSecrets)) {
|
||||
try {
|
||||
if (secret in secretMap) {
|
||||
await NetlifyPublicAPI.deleteVariable(secretSync.connection, params, {
|
||||
key: secret
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: secret
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
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";
|
||||
|
||||
import { NetlifySyncContext } from "./netlify-sync-constants";
|
||||
|
||||
const NetlifySyncDestinationConfigSchema = z.object({
|
||||
accountId: z
|
||||
.string()
|
||||
.min(1, "Account ID is required")
|
||||
.max(255, "Account ID must be less than 255 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.accountId),
|
||||
accountName: z
|
||||
.string()
|
||||
.min(1, "Account Name is required")
|
||||
.max(255, "Account Name must be less than 255 characters")
|
||||
.optional()
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.accountName),
|
||||
siteId: z
|
||||
.string()
|
||||
.min(1, "Site ID is required")
|
||||
.max(255, "Site ID must be less than 255 characters")
|
||||
.optional()
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.siteId),
|
||||
siteName: z
|
||||
.string()
|
||||
.min(1, "Site Name is required")
|
||||
.max(255, "Site Name must be less than 255 characters")
|
||||
.optional()
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.siteName),
|
||||
context: z.nativeEnum(NetlifySyncContext).optional().describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.context)
|
||||
});
|
||||
|
||||
const NetlifySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const NetlifySyncSchema = BaseSecretSyncSchema(SecretSync.Netlify, NetlifySyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Netlify),
|
||||
destinationConfig: NetlifySyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateNetlifySyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Netlify,
|
||||
NetlifySyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: NetlifySyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateNetlifySyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Netlify,
|
||||
NetlifySyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: NetlifySyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const NetlifySyncListItemSchema = z.object({
|
||||
name: z.literal("Netlify"),
|
||||
connection: z.literal(AppConnection.Netlify),
|
||||
destination: z.literal(SecretSync.Netlify),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TNetlifyConnection } from "@app/services/app-connection/netlify";
|
||||
|
||||
import { CreateNetlifySyncSchema, NetlifySyncListItemSchema, NetlifySyncSchema } from "./netlify-sync-schemas";
|
||||
|
||||
export type TNetlifySyncListItem = z.infer<typeof NetlifySyncListItemSchema>;
|
||||
|
||||
export type TNetlifySync = z.infer<typeof NetlifySyncSchema>;
|
||||
|
||||
export type TNetlifySyncInput = z.infer<typeof CreateNetlifySyncSchema>;
|
||||
|
||||
export type TNetlifySyncWithCredentials = TNetlifySync & {
|
||||
connection: TNetlifyConnection;
|
||||
};
|
||||
@@ -27,6 +27,7 @@ export enum SecretSync {
|
||||
Railway = "railway",
|
||||
Checkly = "checkly",
|
||||
DigitalOceanAppPlatform = "digital-ocean-app-platform",
|
||||
Netlify = "netlify",
|
||||
Bitbucket = "bitbucket"
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ 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 { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||
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";
|
||||
@@ -88,6 +89,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION,
|
||||
[SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION,
|
||||
[SecretSync.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION,
|
||||
[SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION,
|
||||
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
@@ -269,6 +271,8 @@ export const SecretSyncFns = {
|
||||
return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
return DigitalOceanAppPlatformSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Netlify:
|
||||
return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
@@ -381,6 +385,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
secretMap = await DigitalOceanAppPlatformSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Netlify:
|
||||
secretMap = await NetlifySyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Bitbucket:
|
||||
secretMap = await BitbucketSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
@@ -473,6 +480,8 @@ export const SecretSyncFns = {
|
||||
return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
return DigitalOceanAppPlatformSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Netlify:
|
||||
return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
|
||||
@@ -30,6 +30,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Railway]: "Railway",
|
||||
[SecretSync.Checkly]: "Checkly",
|
||||
[SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform",
|
||||
[SecretSync.Netlify]: "Netlify",
|
||||
[SecretSync.Bitbucket]: "Bitbucket"
|
||||
};
|
||||
|
||||
@@ -62,6 +63,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Railway]: AppConnection.Railway,
|
||||
[SecretSync.Checkly]: AppConnection.Checkly,
|
||||
[SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean,
|
||||
[SecretSync.Netlify]: AppConnection.Netlify,
|
||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket
|
||||
};
|
||||
|
||||
@@ -94,5 +96,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.Railway]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Checkly]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Netlify]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -117,6 +117,7 @@ import {
|
||||
THumanitecSyncListItem,
|
||||
THumanitecSyncWithCredentials
|
||||
} from "./humanitec";
|
||||
import { TNetlifySync, TNetlifySyncInput, TNetlifySyncListItem, TNetlifySyncWithCredentials } from "./netlify";
|
||||
import {
|
||||
TRailwaySync,
|
||||
TRailwaySyncInput,
|
||||
@@ -178,6 +179,7 @@ export type TSecretSync =
|
||||
| TRailwaySync
|
||||
| TChecklySync
|
||||
| TSupabaseSync
|
||||
| TNetlifySync
|
||||
| TBitbucketSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
@@ -209,6 +211,7 @@ export type TSecretSyncWithCredentials =
|
||||
| TChecklySyncWithCredentials
|
||||
| TSupabaseSyncWithCredentials
|
||||
| TDigitalOceanAppPlatformSyncWithCredentials
|
||||
| TNetlifySyncWithCredentials
|
||||
| TBitbucketSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
@@ -240,6 +243,7 @@ export type TSecretSyncInput =
|
||||
| TChecklySyncInput
|
||||
| TSupabaseSyncInput
|
||||
| TDigitalOceanAppPlatformSyncInput
|
||||
| TNetlifySyncInput
|
||||
| TBitbucketSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
@@ -271,6 +275,7 @@ export type TSecretSyncListItem =
|
||||
| TChecklySyncListItem
|
||||
| TSupabaseSyncListItem
|
||||
| TDigitalOceanAppPlatformSyncListItem
|
||||
| TNetlifySyncListItem
|
||||
| TBitbucketSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
|
||||
Reference in New Issue
Block a user