From 4c5f3859d655e4412f3c6156d9a6e1a9f2c73dd8 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 14 Mar 2025 11:59:13 -0300 Subject: [PATCH 01/18] Add dynamic secrets modal form on secrets overview page --- .../OverviewPage/OverviewPage.tsx | 54 ++++++++- .../components/ActionBar/ActionBar.tsx | 2 +- .../AwsElastiCacheInputForm.tsx | 80 ++++++++++---- .../AwsIamInputForm.tsx | 84 ++++++++++---- .../AzureEntraIdInputForm.tsx | 101 +++++++++++------ .../CassandraInputForm.tsx | 81 ++++++++++---- .../CreateDynamicSecretForm.tsx | 35 +++--- .../ElasticSearchInputForm.tsx | 81 ++++++++++---- .../CreateDynamicSecretForm/LdapInputForm.tsx | 90 +++++++++++---- .../MongoAtlasInputForm.tsx | 81 ++++++++++---- .../MongoDBInputForm.tsx | 104 +++++++++++++----- .../RabbitMqInputForm.tsx | 90 +++++++++++---- .../RedisInputForm.tsx | 81 ++++++++++---- .../SapAseInputForm.tsx | 81 ++++++++++---- .../SapHanaInputForm.tsx | 81 ++++++++++---- .../SnowflakeInputForm.tsx | 81 ++++++++++---- .../SqlDatabaseInputForm.tsx | 81 ++++++++++---- .../CreateDynamicSecretForm/TotpInputForm.tsx | 87 +++++++++++---- 18 files changed, 1002 insertions(+), 373 deletions(-) diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 78a2380d0..a611eb030 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -19,6 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { @@ -49,8 +50,10 @@ import { import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, ProjectPermissionSub, useProjectPermission, + useSubscription, useWorkspace } from "@app/context"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; @@ -71,6 +74,7 @@ import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/ import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types"; import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils"; +import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm"; import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm"; import { CreateSecretForm } from "./components/CreateSecretForm"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; @@ -131,6 +135,7 @@ export const OverviewPage = () => { const [searchFilter, setSearchFilter] = useState(""); const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter); const secretPath = (routerSearch?.secretPath as string) || "/"; + const { subscription } = useSubscription(); const [filter, setFilter] = useState(DEFAULT_FILTER_STATE); const [filterHistory, setFilterHistory] = useState< @@ -178,6 +183,17 @@ export const OverviewPage = () => { }, []); const userAvailableEnvs = currentWorkspace?.environments || []; + const userAvailableDynamicSecretEnvs = userAvailableEnvs.filter((env) => + permission.can( + ProjectPermissionDynamicSecretActions.CreateRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: env.id, + secretPath, + secretName: "*", + secretTags: ["*"] + }) + ) + ); const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs); @@ -249,7 +265,9 @@ export const OverviewPage = () => { "addSecretsInAllEnvs", "addFolder", "misc", - "updateFolder" + "updateFolder", + "addDynamicSecret", + "upgradePlan" ] as const); const handleFolderCreate = async (folderName: string, description: string | null) => { @@ -845,6 +863,22 @@ export const OverviewPage = () => {
+ { /> + handlePopUpToggle("addDynamicSecret", isOpen)} + projectSlug={projectSlug} + environments={userAvailableDynamicSecretEnvs} + secretPath={secretPath} + /> + {subscription && ( + handlePopUpToggle("upgradePlan", isOpen)} + text={ + subscription.slug === null + ? "You can perform this action under an Enterprise license" + : "You can perform this action if you switch to Infisical's Team plan" + } + /> + )}
); }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index d545ba748..28c58ec5f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -655,7 +655,7 @@ export const ActionBar = ({ isOpen={popUp.addDynamicSecret.isOpen} onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)} projectSlug={projectSlug} - environment={environment} + environments={[{ slug: environment, name: "not-used", id: "not-used" }]} secretPath={secretPath} /> 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -59,13 +62,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const AwsElastiCacheInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -87,32 +90,42 @@ export const AwsElastiCacheInputForm = ({ revocationStatement: `{ "UserId": "{{username}}" }` - } + }, + environments: environments.length === 1 ? environments : [] } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (env) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: env.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -299,6 +312,29 @@ export const AwsElastiCacheInputForm = ({ + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index 1acb24e8f..f0590a4e1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -5,9 +5,10 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, TextArea } from "@app/components/v2"; +import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -40,7 +41,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -49,13 +51,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const AwsIamInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -64,31 +66,43 @@ export const AwsIamInputForm = ({ formState: { isSubmitting }, handleSubmit } = useForm({ - resolver: zodResolver(formSchema) + resolver: zodResolver(formSchema), + defaultValues: { + environments: environments.length === 1 ? environments : [] + } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.AwsIam, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AwsIam, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -286,6 +300,30 @@ export const AwsIamInputForm = ({ )} /> + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx index 55cebe173..4ab20c064 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -12,7 +12,7 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input } from "@app/components/v2"; +import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; import { DropdownMenu, DropdownMenuContent, @@ -23,6 +23,7 @@ import { Tooltip } from "@app/components/v2/Tooltip"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ selectedUsers: z.array( @@ -60,7 +61,8 @@ const formSchema = z.object({ name: z .string() .min(1) - .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -69,13 +71,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const AzureEntraIdInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -85,7 +87,10 @@ export const AzureEntraIdInputForm = ({ watch, handleSubmit } = useForm({ - resolver: zodResolver(formSchema) + resolver: zodResolver(formSchema), + defaultValues: { + environments: environments.length === 1 ? environments : [] + } }); const tenantId = watch("provider.tenantId"); const applicationId = watch("provider.applicationId"); @@ -107,38 +112,42 @@ export const AzureEntraIdInputForm = ({ selectedUsers, provider, maxTTL, - defaultTTL + defaultTTL, + environments: selectedEnvs }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isPending) return; - try { - selectedUsers.map(async (user: { id: string; name: string; email: string }) => { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.AzureEntraId, - inputs: { - userId: user.id, - tenantId: provider.tenantId, - email: user.email, - applicationId: provider.applicationId, - clientSecret: provider.clientSecret - } - }, - maxTTL, - name: `${name}-${user.name}`, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment + const promises = selectedEnvs.map(async (environment) => { + if (createDynamicSecret.isPending) return; + try { + selectedUsers.map(async (user: { id: string; name: string; email: string }) => { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.AzureEntraId, + inputs: { + userId: user.id, + tenantId: provider.tenantId, + email: user.email, + applicationId: provider.applicationId, + clientSecret: provider.clientSecret + } + }, + maxTTL, + name: `${name}-${user.name}`, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); }); - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -373,6 +382,30 @@ export const AzureEntraIdInputForm = ({ + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index c8389fb38..a49ab620e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -17,6 +17,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm"; import { AwsIamInputForm } from "./AwsIamInputForm"; @@ -38,7 +39,7 @@ type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; secretPath: string; }; @@ -129,7 +130,7 @@ export const CreateDynamicSecretForm = ({ isOpen, onToggle, projectSlug, - environment, + environments, secretPath }: Props) => { const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider); @@ -197,7 +198,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -215,7 +216,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -233,7 +234,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -251,7 +252,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -269,7 +270,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -287,7 +288,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -305,7 +306,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -323,7 +324,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -341,7 +342,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -359,7 +360,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -377,7 +378,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -395,7 +396,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -413,7 +414,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -432,7 +433,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} @@ -450,7 +451,7 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} /> )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx index eb0a7008f..828f98110 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -9,6 +9,7 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, + FilterableSelect, FormControl, FormLabel, IconButton, @@ -19,6 +20,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const authMethods = [ { @@ -73,7 +75,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -82,13 +85,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const ElasticSearchInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -107,32 +110,42 @@ export const ElasticSearchInputForm = ({ }, roles: ["superuser"], port: 443 - } + }, + environments: environments.length === 1 ? environments : [] } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; const selectedAuthType = watch("provider.auth.type"); @@ -406,6 +419,30 @@ export const ElasticSearchInputForm = ({ )} /> + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx index ef74db5b8..361067d1b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx @@ -7,9 +7,18 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + Input, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; enum CredentialType { Dynamic = "dynamic", @@ -69,7 +78,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -79,7 +89,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const LdapInputForm = ({ @@ -87,7 +97,7 @@ export const LdapInputForm = ({ onCancel, secretPath, projectSlug, - environment + environments }: Props) => { const { control, @@ -107,7 +117,8 @@ export const LdapInputForm = ({ revocationLdif: "", rollbackLdif: "", credentialType: CredentialType.Dynamic - } + }, + environments: environments.length === 1 ? environments : [] } }); @@ -115,26 +126,35 @@ export const LdapInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -369,6 +389,30 @@ export const LdapInputForm = ({ )} /> )} + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx index 4de9b8acc..a6725b652 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx @@ -13,6 +13,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, FormLabel, IconButton, @@ -22,6 +23,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -63,7 +65,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -72,7 +75,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; const ATLAS_SCOPE_TYPES = [ @@ -93,7 +96,7 @@ const ATLAS_SCOPE_TYPES = [ export const MongoAtlasInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -108,7 +111,8 @@ export const MongoAtlasInputForm = ({ defaultValues: { provider: { roles: [{ databaseName: "", roleName: "" }] - } + }, + environments: environments.length === 1 ? environments : [] } }); @@ -124,26 +128,35 @@ export const MongoAtlasInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -438,6 +451,30 @@ export const MongoAtlasInputForm = ({ + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx index b2418f629..0a28a6913 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx @@ -7,9 +7,18 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + FormLabel, + IconButton, + Input, + SecretInput +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -46,7 +55,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -55,13 +65,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const MongoDBDatabaseInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -76,7 +86,8 @@ export const MongoDBDatabaseInputForm = ({ defaultValues: { provider: { roles: [{ roleName: "readWrite" }] - } + }, + environments: environments.length === 1 ? environments : [] } }); @@ -87,33 +98,42 @@ export const MongoDBDatabaseInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.MongoDB, - inputs: { - ...provider, - port: provider?.port ? provider.port : undefined, - roles: provider.roles.map((el) => el.roleName) - } - }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.MongoDB, + inputs: { + ...provider, + port: provider?.port ? provider.port : undefined, + roles: provider.roles.map((el) => el.roleName) + } + }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -321,6 +341,30 @@ export const MongoDBDatabaseInputForm = ({ )} /> + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx index 21f8b40de..0a6b84d41 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -7,9 +7,18 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + FormLabel, + IconButton, + Input, + SecretInput +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -50,7 +59,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -59,13 +69,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const RabbitMqInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -89,32 +99,42 @@ export const RabbitMqInputForm = ({ } }, tags: [] - } + }, + environments: environments.length === 1 ? environments : [] } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; const selectedTags = watch("provider.tags"); @@ -405,6 +425,30 @@ export const RabbitMqInputForm = ({ )} /> + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx index 9d9933b44..35f165056 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx @@ -11,6 +11,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, SecretInput, @@ -18,6 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -50,7 +52,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -59,13 +62,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const RedisInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -81,32 +84,42 @@ export const RedisInputForm = ({ port: 6379, creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all", revocationStatement: "ACL DELUSER {{username}}" - } + }, + environments: environments.length === 1 ? environments : [] } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Redis, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Redis, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -313,6 +326,30 @@ export const RedisInputForm = ({ + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx index efe6e0269..de32565da 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx @@ -11,12 +11,14 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -48,7 +50,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -57,13 +60,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const SapAseInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -82,32 +85,42 @@ sp_adduser '{{username}}', '{{username}}', null; sp_role 'grant', 'mon_role', '{{username}}';`, revocationStatement: `sp_dropuser '{{username}}'; sp_droplogin '{{username}}';` - } + }, + environments: environments.length === 1 ? environments : [] } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -291,6 +304,30 @@ sp_droplogin '{{username}}';` + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx index 1b0bd64b8..d31464665 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx @@ -11,6 +11,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, SecretInput, @@ -18,6 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -50,7 +52,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -59,13 +62,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const SapHanaInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -82,32 +85,42 @@ GRANT "MONITORING" TO {{username}};`, revocationStatement: `REVOKE "MONITORING" FROM {{username}}; DROP USER {{username}};`, renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';" - } + }, + environments: environments.length === 1 ? environments : [] } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SapHana, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapHana, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -311,6 +324,30 @@ DROP USER {{username}};`, + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx index a5f8b25d9..c5077f2ad 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx @@ -13,12 +13,14 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -54,7 +56,8 @@ const formSchema = z.object({ .string() .trim() .min(1) - .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -63,13 +66,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const SnowflakeInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -85,32 +88,42 @@ export const SnowflakeInputForm = ({ "CREATE USER {{username}} PASSWORD = '{{password}}' DEFAULT_ROLE = public DEFAULT_SECONDARY_ROLES = ('ALL') MUST_CHANGE_PASSWORD = FALSE DAYS_TO_EXPIRY = {{expiration}};", revocationStatement: "DROP USER {{username}};", renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};" - } + }, + environments: environments.length === 1 ? environments : [] } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Snowflake, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Snowflake, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: err instanceof Error ? err.message : "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -314,6 +327,30 @@ export const SnowflakeInputForm = ({ + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 4af92b210..416d9c35b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -12,6 +12,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, SecretInput, @@ -22,6 +23,7 @@ import { import { useWorkspace } from "@app/context"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const passwordRequirementsSchema = z .object({ @@ -79,7 +81,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -88,7 +91,7 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; const getSqlStatements = (provider: SqlProviders) => { @@ -145,7 +148,7 @@ const getDefaultPort = (provider: SqlProviders) => { export const SqlDatabaseInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -172,7 +175,8 @@ export const SqlDatabaseInputForm = ({ }, allowedSymbols: "-_.~!*" } - } + }, + environments: environments.length === 1 ? environments : [] } }); @@ -181,26 +185,35 @@ export const SqlDatabaseInputForm = ({ gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id }) ); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (env) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: env.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; const handleDatabaseChange = (type: SqlProviders) => { @@ -649,6 +662,30 @@ export const SqlDatabaseInputForm = ({ + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx index 01d5398f0..4ff83a741 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx @@ -5,9 +5,17 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + Input, + Select, + SelectItem +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; enum ConfigType { URL = "url", @@ -52,7 +60,8 @@ const formSchema = z.object({ .string() .trim() .min(1) - .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environments: z.object({ name: z.string(), slug: z.string() }).array() }); type TForm = z.infer; @@ -62,13 +71,13 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; }; export const TotpInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, projectSlug }: Props) => { @@ -82,7 +91,8 @@ export const TotpInputForm = ({ defaultValues: { provider: { configType: ConfigType.URL - } + }, + environments: environments.length === 1 ? environments : [] } }); @@ -90,26 +100,33 @@ export const TotpInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, provider }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + provider, + environments: selectedEnvs + }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Totp, inputs: provider }, - maxTTL: "24h", - name, - path: secretPath, - defaultTTL: "1m", - projectSlug, - environmentSlug: environment - }); - onCompleted(); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to create dynamic secret" - }); - } + const promises = selectedEnvs.map(async (environment) => { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Totp, inputs: provider }, + maxTTL: "24h", + name, + path: secretPath, + defaultTTL: "1m", + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: err instanceof Error ? err.message : "Failed to create dynamic secret" + }); + } + }); + await Promise.all(promises); }; return ( @@ -295,6 +312,30 @@ export const TotpInputForm = ({

)} + {environments.length > 1 && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} From e39d1a053081e98b5eaeede8abb40d517de9f5b9 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 14 Mar 2025 12:26:20 -0300 Subject: [PATCH 02/18] Fix call of onCompleted after all promises are resolved --- .../secret-manager/OverviewPage/OverviewPage.tsx | 2 +- .../components/ActionBar/ActionBar.tsx | 2 +- .../AwsElastiCacheInputForm.tsx | 8 ++++++-- .../CreateDynamicSecretForm/AwsIamInputForm.tsx | 12 ++++++++---- .../AzureEntraIdInputForm.tsx | 14 +++++++++----- .../CreateDynamicSecretForm/CassandraInputForm.tsx | 12 ++++++++---- .../ElasticSearchInputForm.tsx | 12 ++++++++---- .../CreateDynamicSecretForm/LdapInputForm.tsx | 12 ++++++++---- .../MongoAtlasInputForm.tsx | 12 ++++++++---- .../CreateDynamicSecretForm/MongoDBInputForm.tsx | 12 ++++++++---- .../CreateDynamicSecretForm/RabbitMqInputForm.tsx | 12 ++++++++---- .../CreateDynamicSecretForm/RedisInputForm.tsx | 12 ++++++++---- .../CreateDynamicSecretForm/SapAseInputForm.tsx | 12 ++++++++---- .../CreateDynamicSecretForm/SapHanaInputForm.tsx | 12 ++++++++---- .../CreateDynamicSecretForm/SnowflakeInputForm.tsx | 10 +++++++--- .../SqlDatabaseInputForm.tsx | 8 ++++++-- .../CreateDynamicSecretForm/TotpInputForm.tsx | 10 +++++++--- 17 files changed, 117 insertions(+), 57 deletions(-) diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index a611eb030..12e0100b6 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -866,7 +866,7 @@ export const OverviewPage = () => { + {claimMetadataMappingFields.map(({ id }, index) => ( +
+ { + return ( + + + + ) : undefined + } + isError={Boolean(error)} + errorText={error?.message} + > + field.onChange(e)} + placeholder="property" + /> + + ); + }} + /> + { + return ( + + field.onChange(e)} + placeholder="key1.nested-key2" + /> + + ); + }} + /> + removeClaimMetadataMappingField(index)} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
{accessTokenTrustedIpsFields.map(({ id }, index) => (
)} + + {Object.keys(data.claimMetadataMapping).length && ( + + {JSON.stringify(data.claimMetadataMapping, null, 2)} + + } + > +
+ + + Reveal + +
+
+ )} +
); }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 629c23048..01d477097 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -306,7 +306,10 @@ export const SecretItem = memo( )} /> )} -
+
Date: Fri, 14 Mar 2025 22:10:48 +0530 Subject: [PATCH 06/18] feat: added a simple oidc server --- sink/oidc-server/main.js | 87 ++ sink/oidc-server/package-lock.json | 1708 ++++++++++++++++++++++++++++ sink/oidc-server/package.json | 21 + sink/oidc-server/test-infisical.js | 80 ++ 4 files changed, 1896 insertions(+) create mode 100644 sink/oidc-server/main.js create mode 100644 sink/oidc-server/package-lock.json create mode 100644 sink/oidc-server/package.json create mode 100644 sink/oidc-server/test-infisical.js diff --git a/sink/oidc-server/main.js b/sink/oidc-server/main.js new file mode 100644 index 000000000..0d5f10922 --- /dev/null +++ b/sink/oidc-server/main.js @@ -0,0 +1,87 @@ +import Provider from "oidc-provider"; +import express from "express"; + +const configuration = { + jwks: { + keys: [ + { + kty: "RSA", + use: "sig", + alg: "RS256", + d: "EF2Kky61jzvMYQ_B6ImXzCsQ8uQzbFJrGnB2azlpr_CFStjjUVKP4EKrSCVEasD6SGNJV2QSiNJr7j05nvuGmHMKa__rbU8fqP4qbDahUgCgWOq-zS5tGK6Ifk4II_cZ_V1F-TnrvmcOKMWBiSV-p8i72KpXXucbHGNRwASVs7--M55wp_m1UsybI2jSQ4IgyvGzTnvMmQ_GsX-XoD8u0zGU_4eN3DGc8l6hdxxuSymH0fEeL1Aj0LoCj6teRGF37a2sBQdU6mkNNAuyyirkoDqGZCGJToQLqX4F1FafnzjeIgfdneRa-vuaV380Hhr2rorWnQyBqOO27M5O_VAkJbfRaWJVrXTJ69ZgkU4GPdeYdklVL0HkU6laziTNqNMeAjnt4m51sWokVyJpvdWcb_vJ4NSCsRo7kHOz7g-UvWTXa8UW0DTDliq_TJ3rN4Gv0vn9tBlFfaeuLPpK4VNmRRDRXY_fcuzlnQwYExL9a4V_vCyGmabdb7PrUFPBcjR5", + dp: "SX52TkZEc_eLIk5gYrKjAC643LJIw1RxMBWWewRSGLn_rbrH1he3hy7AGDUV6Uon7zkNh9R5GBVuxmlluBRAGbrhIXAAf8sWeyma3F6FIAt-MH_VkfW5K2p88PLOyVGljlv8-Z3wzdKYOlDP4yFU18LqGMqaRSDLDGhILkuZhjLYA40sfYJeJTi_HVP5UyWL4ohayqUWCT2W3DgeDDThYHmufOaqlrSLhUst6uez_cDz0BXAYIZvUuPVL_n1-_px", + dq: "K1KYU77I6yyPA2u32rc0exp_TCG59hhpWxrmXN8yTXWyq_xYBhCJA_nHdY8UV25Hmd7q0iX2i8y2cCAFNWA5UWiSiNg9-fKRLI2nz53IM4dGfssOLwUk66wzX8r_u3XiLZsO7XNNtQZdcZmF0YuNTtzEdiNDhaOyHiwwHgShL36WNmUn00mZR__G5Qk60VvI8vsbvJU9xRnWuEVS1wRgyD7v6Nl9nIxb8N7oibCdTJLmgnRXPWvArsW0cJ-NURfr", + e: "AQAB", + n: "2QwX-NBMkQYedGpbPvHL7Ca0isvfmLC7lSc8XSOCLmCUIf6Bk_pdCNx2kxsmT81IoA8CfvJLHQj5vWKoVDFMLfwo4IujvsC3m2IrEg6jERE-YHfC3W5jKZtmzQYpfx5vC2_XTmcyPigtyaNVsftGfycES3B_tvphNsFmQcJjVGOsJQXXqh_TDv6FMcH4m9pngyw6wfe3GgAKA0dRTSfD0h7wLdNCeuid53lLpkQypTNdZ6_PiCMu2gr_cH5M0MPZtBb2TW12_2zOabExK1lI5-HvdPtbMT4Qzs2nd2NkjcWmlbKRZzq6IzyWt7W2EnfZDsi61PHECtTb-EQN2icl8Wnsp-0Bw66yviAOj0gn3X5hRLx-TknT_PnWMou17l5GoAojKDezcTW0iLlrfs2ixFlY28u7WklUN8uYhHvwgON6fsdefG-3bPpiRLBPZ_tgXa4doALsCwfXu2oz0vYktk31A-UYv92uJsKSUbK0_8ODTN0rslCqCYN_1a_aVt2P", + p: "--L5BX8juLlGJk8hdPgEUmJjD7SsZuMrdq3cSibkkbaWUE5CQQ7vhLPr2dWCS1jUnY9WyoCx9QCZvhTHjORX50ykkOyBso9VJjWvYPjsrPpF7_Y6V0dKlblDmbbmRT9BW-MgjbwTivu3c2OpMXh2XLF-FOTq3t3Brs7SRnhTkD6GBDFf3X95J0PF7NELa9z2-kzPSDYz3k-9FepXnRPBM_ViDzlRw4eKUdylVuhzGbC2TRSmab9BRP0wipQKd-f5", + q: "3Jd5CRJpQV3xUi3FiHHAwcjfsRkfXMrxfaXt0PjX2xWzxscYiDcyCF6VhHTAGsiq5SOtCp3l5mg6A9PzdR53AzM2-706D82fMwiUZvsLOVTepXkgriP_xw7rDlkOeAvjB80sL2G9scFliTzzRZ8I8E79A8DxZihfB75AIN9ijklEihnwxfhp2EgO5MYEyQRcqU1TT8wD8ekLMzd-kJUWyTz3BogiVJH__BQoB6kaDyjvQoxBgwh0hi72t9H5XqPH", + qi: "cwK0jhzwbu8BaTmTQhwfGiqwNN3v9F4nUQ4dtnBYRI6zlki4cLb2Mf9-VhyEsUYhhdTm8R7RwO9m5Xct3gEfozdk35wuvkVwkZgL3Uho5asao0xi4aENeUk5DCkU-paO3yLSDhIs9YYuYIDjUX6QuMCPjomypuE3SRm-Dg1PGOxYvX3w_P-0kd5iBFrm4jwGTZViFOr8tl_dXgDRDWDgofOYOYcmUv2_0zt1aO3j5dhEpwdkyuDMLfVZNpJQyopJ", + kid: "f262a3214213d194c92991d6735b153b", + }, + ], + }, + features: { + clientCredentials: { + enabled: true, + }, + introspection: { + enabled: true, + }, + resourceIndicators: { + enabled: true, + getResourceServerInfo(ctx, resourceIndicator) { + if (resourceIndicator === "urn:api") { + return { + scope: "read", + audience: "urn:api", + accessTokenTTL: 1 * 60 * 60, // 1 hour + accessTokenFormat: "jwt", + }; + } + + throw new errors.InvalidTarget(); + }, + }, + }, + clients: [ + { + client_id: "app", + client_secret: "a_secret", + grant_types: ["client_credentials"], + redirect_uris: [], + response_types: [], + }, + { + client_id: "oidc_client", + client_secret: "a_different_secret", + grant_types: ["authorization_code"], + response_types: ["code"], + redirect_uris: ["http://localhost:3001/cb"], + }, + ], + claims: { + profile: [ + "birthdate", + "family_name", + "gender", + "given_name", + "locale", + "middle_name", + "name", + "nickname", + "picture", + "preferred_username", + "profile", + "updated_at", + "website", + "zoneinfo", + ], + email: ["email", "email_verified"], + }, +}; + +const oidc = new Provider("http://localhost:3000", configuration); + +const app = express(); +app.use("/oidc", oidc.callback()); +app.listen(3000); diff --git a/sink/oidc-server/package-lock.json b/sink/oidc-server/package-lock.json new file mode 100644 index 000000000..2be29633b --- /dev/null +++ b/sink/oidc-server/package-lock.json @@ -0,0 +1,1708 @@ +{ + "name": "oidc-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "oidc-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.8.3", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "form-data": "^4.0.2", + "jose": "^6.0.10", + "oidc-provider": "^8.8.1" + } + }, + "node_modules/@koa/cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", + "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "license": "MIT", + "dependencies": { + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@koa/router": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", + "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", + "license": "MIT", + "dependencies": { + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "path-to-regexp": "^6.3.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@koa/router/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jose": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz", + "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/koa": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.0.tgz", + "integrity": "sha512-Afhqq0Vq3W7C+/rW6IqHVBDLzqObwZ07JaUNUEF8yCQ6afiyFE3RAy+i7V0E46XOWlH7vPWn/x0vsZwNy6PWxw==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "license": "MIT" + }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/koa/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz", + "integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/oidc-provider": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.8.1.tgz", + "integrity": "sha512-qVChpayTwojUREJxLkFofUSK8kiSRIdzPrVSsoGibqRHl/YO60ege94OZS8vh7zaK+zxcG/Gu8UMaYB5ulohCQ==", + "license": "MIT", + "dependencies": { + "@koa/cors": "^5.0.0", + "@koa/router": "^13.1.0", + "debug": "^4.4.0", + "eta": "^3.5.0", + "got": "^13.0.0", + "jose": "^5.9.6", + "jsesc": "^3.1.0", + "koa": "^2.15.4", + "nanoid": "^5.0.9", + "object-hash": "^3.0.0", + "oidc-token-hash": "^5.0.3", + "quick-lru": "^7.0.0", + "raw-body": "^3.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/oidc-provider/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/oidc-provider/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oidc-provider/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/oidc-provider/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/oidc-provider/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-lru": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.0.0.tgz", + "integrity": "sha512-MX8gB7cVYTrYcFfAnfLlhRd0+Toyl8yX8uBx1MrX7K0jegiz9TumwOK27ldXrgDlHRdVi+MqU9Ssw6dr4BNreg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + } + } +} diff --git a/sink/oidc-server/package.json b/sink/oidc-server/package.json new file mode 100644 index 000000000..514629d46 --- /dev/null +++ b/sink/oidc-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "oidc-server", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node main.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.8.3", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "form-data": "^4.0.2", + "jose": "^6.0.10", + "oidc-provider": "^8.8.1" + } +} diff --git a/sink/oidc-server/test-infisical.js b/sink/oidc-server/test-infisical.js new file mode 100644 index 000000000..a2d8df022 --- /dev/null +++ b/sink/oidc-server/test-infisical.js @@ -0,0 +1,80 @@ +import axios from "axios"; +import { Buffer } from "buffer"; +import querystring from "querystring"; + +// Configuration +const config = { + issuer: "http://localhost:3000/oidc", + tokenEndpoint: "http://localhost:3000/oidc/token", + clientId: "app", + clientSecret: "a_secret", +}; + +// Client credentials flow for machine identity +async function getMachineToken() { + try { + // Use application/x-www-form-urlencoded format as required by the OIDC spec + const data = querystring.stringify({ + grant_type: "client_credentials", + scope: "read", + resource: "urn:api", + }); + + const authHeader = + "Basic " + + Buffer.from(`${config.clientId}:${config.clientSecret}`).toString( + "base64", + ); + + const response = await axios.post(config.tokenEndpoint, data, { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: authHeader, + }, + }); + + console.log("Successfully obtained token:"); + console.log("Access Token:", response.data.access_token); + console.log("Token Type:", response.data.token_type); + console.log("Expires In:", response.data.expires_in, "seconds"); + console.log("Scope:", response.data.scope); + + return response.data; + } catch (error) { + console.error("Error obtaining token:"); + if (error.response && error.response.data) { + console.error(error.response.data); + } else { + console.error(error.message); + } + throw error; + } +} + +// Test the machine identity authentication +async function testMachineIdentity() { + try { + // Get token using client credentials + const token = await getMachineToken(); + + const loginData = querystring.stringify({ + identityId: "5d81d5cc-602f-4af7-b242-ab7c1331b430", + jwt: token.access_token, + }); + + const response = await axios({ + method: "post", + url: `http://localhost:8080/api/v1/auth/oidc-auth/login`, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data: loginData, + }); + console.log(response.data); + } catch (error) { + console.error("Error in test:", error.message); + } +} + +// Run the test +testMachineIdentity(); From 6905029455550879c08b32ccbb18036d16e75d2a Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 14 Mar 2025 14:59:19 -0300 Subject: [PATCH 07/18] Change overview dynamic secret creation modal to set only one env --- .../AwsElastiCacheInputForm.tsx | 50 ++++++------- .../AwsIamInputForm.tsx | 51 ++++++-------- .../AzureEntraIdInputForm.tsx | 70 ++++++++----------- .../CassandraInputForm.tsx | 51 ++++++-------- .../ElasticSearchInputForm.tsx | 50 ++++++------- .../CreateDynamicSecretForm/LdapInputForm.tsx | 50 ++++++------- .../MongoAtlasInputForm.tsx | 50 ++++++------- .../MongoDBInputForm.tsx | 64 ++++++++--------- .../RabbitMqInputForm.tsx | 50 ++++++------- .../RedisInputForm.tsx | 50 ++++++------- .../SapAseInputForm.tsx | 50 ++++++------- .../SapHanaInputForm.tsx | 50 ++++++------- .../SnowflakeInputForm.tsx | 50 ++++++------- .../SqlDatabaseInputForm.tsx | 51 ++++++-------- .../CreateDynamicSecretForm/TotpInputForm.tsx | 54 ++++++-------- 15 files changed, 335 insertions(+), 456 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx index ae3ddac05..c326f9dbe 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx @@ -53,7 +53,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -91,7 +91,7 @@ export const AwsElastiCacheInputForm = ({ "UserId": "{{username}}" }` }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -102,33 +102,26 @@ export const AwsElastiCacheInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - hasErrors = true; - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -319,19 +312,18 @@ export const AwsElastiCacheInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} /> diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index 1f2e5b0a8..ad8cd1b0e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -42,7 +42,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -68,7 +68,7 @@ export const AwsIamInputForm = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -79,33 +79,27 @@ export const AwsIamInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.AwsIam, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - hasErrors = true; - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - } - }); - await Promise.all(promises); - if (!hasErrors) { + + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AwsIam, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -307,19 +301,18 @@ export const AwsIamInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx index f681a75a9..4df6d7b5a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -62,7 +62,7 @@ const formSchema = z.object({ .string() .min(1) .refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -89,7 +89,7 @@ export const AzureEntraIdInputForm = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); const tenantId = watch("provider.tenantId"); @@ -113,44 +113,37 @@ export const AzureEntraIdInputForm = ({ provider, maxTTL, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - selectedUsers.map(async (user: { id: string; name: string; email: string }) => { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.AzureEntraId, - inputs: { - userId: user.id, - tenantId: provider.tenantId, - email: user.email, - applicationId: provider.applicationId, - clientSecret: provider.clientSecret - } - }, - maxTTL, - name: `${name}-${user.name}`, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); + try { + selectedUsers.map(async (user: { id: string; name: string; email: string }) => { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.AzureEntraId, + inputs: { + userId: user.id, + tenantId: provider.tenantId, + email: user.email, + applicationId: provider.applicationId, + clientSecret: provider.clientSecret + } + }, + maxTTL, + name: `${name}-${user.name}`, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -389,19 +382,18 @@ export const AzureEntraIdInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx index a770d67b2..cd8486337 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx @@ -55,7 +55,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -91,7 +91,7 @@ export const CassandraInputForm = ({ resolver: zodResolver(formSchema), defaultValues: { provider: getSqlStatements(), - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -102,33 +102,27 @@ export const CassandraInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Cassandra, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Cassandra, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -365,19 +359,18 @@ export const CassandraInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx index 0fddf332c..eea23a2e5 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -76,7 +76,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -111,7 +111,7 @@ export const ElasticSearchInputForm = ({ roles: ["superuser"], port: 443 }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -122,33 +122,26 @@ export const ElasticSearchInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -426,19 +419,18 @@ export const ElasticSearchInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx index cfa9fd45f..ca0e1befb 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx @@ -79,7 +79,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -118,7 +118,7 @@ export const LdapInputForm = ({ rollbackLdif: "", credentialType: CredentialType.Dynamic }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -131,33 +131,26 @@ export const LdapInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -396,19 +389,18 @@ export const LdapInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx index 53c716200..524f086d4 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx @@ -66,7 +66,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -112,7 +112,7 @@ export const MongoAtlasInputForm = ({ provider: { roles: [{ databaseName: "", roleName: "" }] }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -133,33 +133,26 @@ export const MongoAtlasInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -458,19 +451,18 @@ export const MongoAtlasInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx index 46597291a..87747c4db 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx @@ -56,7 +56,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -87,7 +87,7 @@ export const MongoDBDatabaseInputForm = ({ provider: { roles: [{ roleName: "readWrite" }] }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -103,40 +103,33 @@ export const MongoDBDatabaseInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.MongoDB, - inputs: { - ...provider, - port: provider?.port ? provider.port : undefined, - roles: provider.roles.map((el) => el.roleName) - } - }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.MongoDB, + inputs: { + ...provider, + port: provider?.port ? provider.port : undefined, + roles: provider.roles.map((el) => el.roleName) + } + }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -348,19 +341,18 @@ export const MongoDBDatabaseInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx index ffe2164e8..fdecd23ab 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -60,7 +60,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -100,7 +100,7 @@ export const RabbitMqInputForm = ({ }, tags: [] }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -111,33 +111,26 @@ export const RabbitMqInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -432,19 +425,18 @@ export const RabbitMqInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx index 82a5f2bdf..b0f7bd5d6 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx @@ -53,7 +53,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -85,7 +85,7 @@ export const RedisInputForm = ({ creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all", revocationStatement: "ACL DELUSER {{username}}" }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -96,33 +96,26 @@ export const RedisInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Redis, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Redis, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -333,19 +326,18 @@ export const RedisInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx index 126b95712..0d8aad9b8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx @@ -51,7 +51,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -86,7 +86,7 @@ sp_role 'grant', 'mon_role', '{{username}}';`, revocationStatement: `sp_dropuser '{{username}}'; sp_droplogin '{{username}}';` }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -97,33 +97,26 @@ sp_droplogin '{{username}}';` maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -311,19 +304,18 @@ sp_droplogin '{{username}}';` {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx index d4d45ea75..565dc849f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx @@ -53,7 +53,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -86,7 +86,7 @@ GRANT "MONITORING" TO {{username}};`, DROP USER {{username}};`, renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';" }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -97,33 +97,26 @@ DROP USER {{username}};`, maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SapHana, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret in environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapHana, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -331,19 +324,18 @@ DROP USER {{username}};`, {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx index d7e55c183..e9442cbc3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx @@ -57,7 +57,7 @@ const formSchema = z.object({ .trim() .min(1) .refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -89,7 +89,7 @@ export const SnowflakeInputForm = ({ revocationStatement: "DROP USER {{username}};", renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};" }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -100,33 +100,26 @@ export const SnowflakeInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Snowflake, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to create dynamic secret" - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Snowflake, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: err instanceof Error ? err.message : "Failed to create dynamic secret" + }); } }; @@ -334,19 +327,18 @@ export const SnowflakeInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index d80d92c4a..d5088f364 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -82,7 +82,7 @@ const formSchema = z.object({ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -176,7 +176,7 @@ export const SqlDatabaseInputForm = ({ allowedSymbols: "-_.~!*" } }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : undefined } }); @@ -190,33 +190,27 @@ export const SqlDatabaseInputForm = ({ maxTTL, provider, defaultTTL, - environments: selectedEnvs + environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: env.slug - }); - } catch { - createNotification({ - type: "error", - text: `Failed to create dynamic secret for environment ${env.name}` - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); } }; @@ -669,19 +663,18 @@ export const SqlDatabaseInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx index 4609d2604..8207a117e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx @@ -61,7 +61,7 @@ const formSchema = z.object({ .trim() .min(1) .refine((val) => val.toLowerCase() === val, "Must be lowercase"), - environments: z.object({ name: z.string(), slug: z.string() }).array() + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -92,7 +92,7 @@ export const TotpInputForm = ({ provider: { configType: ConfigType.URL }, - environments: environments.length === 1 ? environments : [] + environment: environments.length === 1 ? environments[0] : environments[0] } }); @@ -100,36 +100,25 @@ export const TotpInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ - name, - provider, - environments: selectedEnvs - }: TForm) => { + const handleCreateDynamicSecret = async ({ name, provider, environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - let hasErrors = false; - const promises = selectedEnvs.map(async (env) => { - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Totp, inputs: provider }, - maxTTL: "24h", - name, - path: secretPath, - defaultTTL: "1m", - projectSlug, - environmentSlug: env.slug - }); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to create dynamic secret" - }); - hasErrors = true; - } - }); - await Promise.all(promises); - if (!hasErrors) { + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Totp, inputs: provider }, + maxTTL: "24h", + name, + path: secretPath, + defaultTTL: "1m", + projectSlug, + environmentSlug: environment.slug + }); onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: err instanceof Error ? err.message : "Failed to create dynamic secret" + }); } }; @@ -319,19 +308,18 @@ export const TotpInputForm = ({ {environments.length > 1 && ( ( option.name} getOptionValue={(option) => option.slug} menuPlacement="top" From fd1afc2cbe9e9fdc3427454cb73b514ff148b6fe Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 14 Mar 2025 11:04:49 -0700 Subject: [PATCH 08/18] fix: handle disabled/destroyed values in gcp sync --- backend/src/services/secret-sync/gcp/gcp-sync-fns.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index f0eb3ce88..5e6006d86 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -71,8 +71,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); } catch (error) { - // when a secret in GCP has no versions, we treat it as if it's a blank value - if (error instanceof AxiosError && error.response?.status === 404) { + // when a secret in GCP has no versions, or is disabled/destroyed, we treat it as if it's a blank value + if ( + error instanceof AxiosError && + (error.response?.status === 404 || + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (error.response?.status === 400 && error.response.data.error.status === "FAILED_PRECONDITION")) + ) { res[key] = ""; } else { throw new SecretSyncError({ From 48943b4d78cce286ad403913befe2c488d1b8892 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 14 Mar 2025 11:26:59 -0700 Subject: [PATCH 09/18] improvement: refine status check --- backend/src/services/secret-sync/gcp/gcp-sync-fns.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index 5e6006d86..867b0b870 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -75,8 +75,11 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden if ( error instanceof AxiosError && (error.response?.status === 404 || - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (error.response?.status === 400 && error.response.data.error.status === "FAILED_PRECONDITION")) + (error.response?.status === 400 && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + error.response.data.error.status === "FAILED_PRECONDITION" && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call + error.response.data.error.message.match(/(?:disabled|destroyed)/i))) ) { res[key] = ""; } else { From d4c69d8e5d3c0cb900d2377105b46723fc899770 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 14 Mar 2025 15:24:21 -0700 Subject: [PATCH 10/18] feature: disable secret deletion sync option --- backend/src/lib/api-docs/constants.ts | 3 +- .../aws-parameter-store-sync-fns.ts | 2 + .../aws-secrets-manager-sync-fns.ts | 2 + .../azure-app-configuration-sync-fns.ts | 2 + .../azure-key-vault-sync-fns.ts | 2 + .../databricks/databricks-sync-fns.ts | 2 + .../services/secret-sync/gcp/gcp-sync-fns.ts | 3 ++ .../secret-sync/github/github-sync-fns.ts | 14 +++--- .../humanitec/humanitec-sync-fns.ts | 2 + .../secret-sync/secret-sync-schemas.ts | 3 +- .../secret-syncs/aws-parameter-store.mdx | 3 +- .../secret-syncs/aws-secrets-manager.mdx | 2 + .../secret-syncs/azure-app-configuration.mdx | 3 +- .../secret-syncs/azure-key-vault.mdx | 3 +- docs/integrations/secret-syncs/databricks.mdx | 1 + .../secret-syncs/gcp-secret-manager.mdx | 1 + docs/integrations/secret-syncs/github.mdx | 1 + docs/integrations/secret-syncs/humanitec.mdx | 1 + .../SecretSyncOptionsFields.tsx | 42 +++++++++++++++++- .../SecretSyncReviewFields.tsx | 6 +++ .../forms/schemas/base-secret-sync-schema.ts | 3 +- .../hooks/api/secretSyncs/types/root-sync.ts | 1 + .../SecretSyncOptionsSection.tsx | 44 ++++++++++--------- 23 files changed, 112 insertions(+), 34 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 7a350938f..de7411cac 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1725,7 +1725,8 @@ export const SecretSyncs = { SYNC_OPTIONS: (destination: SecretSync) => { const destinationName = SECRET_SYNC_NAME_MAP[destination]; return { - initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.` + initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, + disableSecretDeletion: `Enable this flag to prevent removal of secrets when syncing from the ${destinationName} destination.` }; }, ADDITIONAL_SYNC_OPTIONS: { diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index 9ca035774..7e77bd256 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -382,6 +382,8 @@ export const AwsParameterStoreSyncFns = { } } + if (syncOptions.disableSecretDeletion) return; + const parametersToDelete: AWS.SSM.Parameter[] = []; for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts index d4f272475..7cea12d1b 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts @@ -396,6 +396,8 @@ export const AwsSecretsManagerSyncFns = { } } + if (syncOptions.disableSecretDeletion) return; + for await (const secretKey of Object.keys(awsSecretsRecord)) { if (!(secretKey in secretMap) || !secretMap[secretKey].value) { try { diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts index 8c0587599..64d82c125 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -136,6 +136,8 @@ export const azureAppConfigurationSyncFactory = ({ } } + if (secretSync.syncOptions.disableSecretDeletion) return; + for await (const key of Object.keys(azureAppConfigSecrets)) { const azureSecret = azureAppConfigSecrets[key]; if ( diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts index 21fef297a..b50d78bb2 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -189,6 +189,8 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur }); } + if (secretSync.syncOptions.disableSecretDeletion) return; + for await (const deleteSecretKey of deleteSecrets.filter( (secret) => !setSecrets.find((setSecret) => setSecret.key === secret) )) { diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts index 2d450b2b1..2ee7977a4 100644 --- a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts +++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts @@ -112,6 +112,8 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri accessToken }); + if (secretSync.syncOptions.disableSecretDeletion) return; + for await (const secret of databricksSecretKeys) { if (!(secret.key in secretMap)) { await deleteDatabricksSecrets({ diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index f0eb3ce88..27ad2cba6 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -147,6 +147,9 @@ export const GcpSyncFns = { for await (const key of Object.keys(gcpSecrets)) { try { if (!(key in secretMap) || !secretMap[key].value) { + // eslint-disable-next-line no-continue + if (secretSync.syncOptions.disableSecretDeletion) continue; + // case: delete secret await request.delete( `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index a09a41163..1fe922de5 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -192,12 +192,6 @@ export const GithubSyncFns = { const publicKey = await getPublicKey(client, secretSync); - for await (const encryptedSecret of encryptedSecrets) { - if (!(encryptedSecret.name in secretMap)) { - await deleteSecret(client, secretSync, encryptedSecret); - } - } - await sodium.ready.then(async () => { for await (const key of Object.keys(secretMap)) { // convert secret & base64 key to Uint8Array. @@ -224,6 +218,14 @@ export const GithubSyncFns = { } } }); + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const encryptedSecret of encryptedSecrets) { + if (!(encryptedSecret.name in secretMap)) { + await deleteSecret(client, secretSync, encryptedSecret); + } + } }, getSecrets: async (secretSync: TGitHubSyncWithCredentials) => { throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts index a07d2d0b5..5fa0a3d63 100644 --- a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -196,6 +196,8 @@ export const HumanitecSyncFns = { } } + if (secretSync.syncOptions.disableSecretDeletion) return; + for await (const humanitecSecret of humanitecSecrets) { if (!secretMap[humanitecSecret.key]) { await deleteSecret(secretSync, humanitecSecret); diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 8ea3a4f0b..50ff3f307 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -23,7 +23,8 @@ const BaseSyncOptionsSchema = ({ initialSyncBehavior: (canImportSecrets ? z.nativeEnum(SecretSyncInitialSyncBehavior) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) - ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior) + ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior), + disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion) }); const schema = merge ? baseSchema.merge(merge) : baseSchema; diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 165998841..831d0c752 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -43,8 +43,9 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with. - **Tags**: Optional resource tags to add to parameters synced by Infisical. - **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical. - Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. + Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. - **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. 6. Configure the **Details** of your Parameter Store Sync, then click **Next**. ![Configure Details](/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png) diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index b8df0e8b3..a301ee86b 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -46,7 +46,9 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with. - **Tags**: Optional tags to add to secrets synced by Infisical. - **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical. + Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. - **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. 6. Configure the **Details** of your Secrets Manager Sync, then click **Next**. ![Configure Details](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png) diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index 3b443c672..35a577872 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -6,7 +6,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic **Prerequisites:** - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure App Configuration. + - Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration) The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal @@ -50,6 +50,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - **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. 6. Configure the **Details** of your Azure App Configuration Sync, then click **Next**. ![Configure Details](/images/secret-syncs/azure-app-configuration/app-config-details.png) diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index 9b28f841f..5f55a73ae 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -6,7 +6,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." **Prerequisites:** - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure Key Vault. + - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault) The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal @@ -52,6 +52,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - **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. 6. Configure the **Details** of your Azure Key Vault Sync, then click **Next**. ![Configure Details](/images/secret-syncs/azure-key-vault/vault-details.png) diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx index 148542708..c9db5f88a 100644 --- a/docs/integrations/secret-syncs/databricks.mdx +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -47,6 +47,7 @@ description: "Learn how to configure a Databricks Sync for Infisical." Databricks does not support importing secrets. - **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. 6. Configure the **Details** of your Databricks Sync, then click **Next**. ![Configure Details](/images/secret-syncs/databricks/databricks-details.png) diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index bcb3b87bd..72c932116 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -43,6 +43,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict. - **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict. - **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. 6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**. ![Configure Details](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png) diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index d7d7afd86..d55ec3d0b 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -63,6 +63,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." GitHub does not support importing secrets. - **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. 6. Configure the **Details** of your GitHub Sync, then click **Next**. ![Configure Details](/images/secret-syncs/github/github-details.png) diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx index 0edc2098e..e8cd7eafc 100644 --- a/docs/integrations/secret-syncs/humanitec.mdx +++ b/docs/integrations/secret-syncs/humanitec.mdx @@ -56,6 +56,7 @@ description: "Learn how to configure a Humanitec Sync for Infisical." Humanitec does not support importing secrets. - **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. 6. Configure the **Details** of your Humanitec Sync, then click **Next**. ![Configure Details](/images/secret-syncs/humanitec/humanitec-details.png) diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index d3213caf7..802c9cc7c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -1,9 +1,9 @@ import { ReactNode } from "react"; import { Controller, useFormContext } from "react-hook-form"; -import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; +import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { FormControl, Select, SelectItem } from "@app/components/v2"; +import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs"; @@ -116,6 +116,44 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { )} {AdditionalSyncOptionsFieldsComponent} + { + return ( + + +

+ Disable Secret Deletion{" "} + +

+ When enabled, Infisical will not{" "} + remove secrets from the sync destination. +

+

+ Enable this option if you intend to manage some secrets manually outside + of Infisical. +

+ + } + > + +
+

+ + + ); + }} + /> {/* ( { secretPath, syncOptions: { // appendSuffix, prependPrefix, + disableSecretDeletion, initialSyncBehavior }, destination, @@ -111,6 +112,11 @@ export const SecretSyncReviewFields = () => { {/* {prependPrefix} {appendSuffix} */} {AdditionalSyncOptionsFieldsComponent} + {disableSecretDeletion && ( + + Enabled + + )}
diff --git a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts index b898fe91f..bf72321ce 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts @@ -7,7 +7,8 @@ export const BaseSecretSyncSchema = { const baseSyncOptionsSchema = z.object({ - initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior) + initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior), + disableSecretDeletion: z.boolean().optional().default(false) // scott: removed temporarily for evaluation of template formatting // prependPrefix: z // .string() diff --git a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts index 49cf9c978..dfbbcd063 100644 --- a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts @@ -3,6 +3,7 @@ import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/ export type RootSyncOptions = { initialSyncBehavior: SecretSyncInitialSyncBehavior; + disableSecretDeletion?: boolean; // prependPrefix?: string; // appendSuffix?: string; }; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index be48428ea..139fcf916 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ProjectPermissionCan } from "@app/components/permissions"; import { SecretSyncLabel } from "@app/components/secret-syncs"; -import { IconButton } from "@app/components/v2"; +import { Badge, IconButton } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs"; @@ -24,7 +24,8 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = syncOptions: { // appendSuffix, // prependPrefix, - initialSyncBehavior + initialSyncBehavior, + disableSecretDeletion } } = secretSync; @@ -58,24 +59,22 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =

Sync Options

- {AdditionalSyncOptionsComponent && ( - - {(isAllowed) => ( - - - - )} - - )} + + {(isAllowed) => ( + + + + )} +
@@ -85,6 +84,11 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = {/* {prependPrefix} {appendSuffix} */} {AdditionalSyncOptionsComponent} + {disableSecretDeletion && ( + + Enabled + + )}
From b89212a0c9d459bd54087501e65678bb15b4da8d Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 14 Mar 2025 15:27:47 -0700 Subject: [PATCH 11/18] improvement: improve property description --- backend/src/lib/api-docs/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index de7411cac..1a7ebf1ef 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1726,7 +1726,7 @@ export const SecretSyncs = { const destinationName = SECRET_SYNC_NAME_MAP[destination]; return { initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, - disableSecretDeletion: `Enable this flag to prevent removal of secrets when syncing from the ${destinationName} destination.` + disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.` }; }, ADDITIONAL_SYNC_OPTIONS: { From cc7d0d752f306ba5f829ddfff9f8bf7728677e58 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 14 Mar 2025 15:30:31 -0700 Subject: [PATCH 12/18] improvement: improve tooltip description --- .../forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 802c9cc7c..b14bce809 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -137,7 +137,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { <>

When enabled, Infisical will not{" "} - remove secrets from the sync destination. + remove secrets from the destination during a sync.

Enable this option if you intend to manage some secrets manually outside From e53439d586c24413182c57520ddbb1a11d556a08 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 14 Mar 2025 23:18:57 -0300 Subject: [PATCH 13/18] Improvements on dynamic secrets overview --- .../OverviewPage/OverviewPage.tsx | 49 ++++++++++--------- .../components/ActionBar/ActionBar.tsx | 1 + .../AwsElastiCacheInputForm.tsx | 8 +-- .../AwsIamInputForm.tsx | 8 +-- .../AzureEntraIdInputForm.tsx | 8 +-- .../CassandraInputForm.tsx | 8 +-- .../CreateDynamicSecretForm.tsx | 19 ++++++- .../ElasticSearchInputForm.tsx | 8 +-- .../CreateDynamicSecretForm/LdapInputForm.tsx | 8 +-- .../MongoAtlasInputForm.tsx | 8 +-- .../MongoDBInputForm.tsx | 8 +-- .../RabbitMqInputForm.tsx | 8 +-- .../RedisInputForm.tsx | 8 +-- .../SapAseInputForm.tsx | 8 +-- .../SapHanaInputForm.tsx | 8 +-- .../SnowflakeInputForm.tsx | 8 +-- .../SqlDatabaseInputForm.tsx | 8 +-- .../CreateDynamicSecretForm/TotpInputForm.tsx | 8 +-- 18 files changed, 121 insertions(+), 68 deletions(-) diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 12e0100b6..34012bb2f 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -187,10 +187,8 @@ export const OverviewPage = () => { permission.can( ProjectPermissionDynamicSecretActions.CreateRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { - environment: env.id, - secretPath, - secretName: "*", - secretTags: ["*"] + environment: env.slug, + secretPath }) ) ); @@ -863,42 +861,49 @@ export const OverviewPage = () => {

- {(isAllowed) => ( )} + + +
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 0cf131ba8..681ad99e3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -657,6 +657,7 @@ export const ActionBar = ({ projectSlug={projectSlug} environments={[{ slug: environment, name: environment, id: "not-used" }]} secretPath={secretPath} + isSingleEnvironmentMode /> { const { control, @@ -91,7 +93,7 @@ export const AwsElastiCacheInputForm = ({ "UserId": "{{username}}" }` }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -309,7 +311,7 @@ export const AwsElastiCacheInputForm = ({ - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -68,7 +70,7 @@ export const AwsIamInputForm = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -298,7 +300,7 @@ export const AwsIamInputForm = ({ )} /> - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -89,7 +91,7 @@ export const AzureEntraIdInputForm = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); const tenantId = watch("provider.tenantId"); @@ -379,7 +381,7 @@ export const AzureEntraIdInputForm = ({
- {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { @@ -81,7 +82,8 @@ export const CassandraInputForm = ({ onCancel, environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -91,7 +93,7 @@ export const CassandraInputForm = ({ resolver: zodResolver(formSchema), defaultValues: { provider: getSqlStatements(), - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -356,7 +358,7 @@ export const CassandraInputForm = ({ - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider); const [selectedProvider, setSelectedProvider] = useState(null); @@ -199,6 +201,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -217,6 +220,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -235,6 +239,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -253,6 +258,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -271,6 +277,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -289,6 +296,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -307,6 +315,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -325,6 +334,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -343,6 +353,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -361,6 +372,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -379,6 +391,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -397,6 +410,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -415,6 +429,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -434,6 +449,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -452,6 +468,7 @@ export const CreateDynamicSecretForm = ({ projectSlug={projectSlug} secretPath={secretPath} environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx index eea23a2e5..a7c84cb50 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -86,6 +86,7 @@ type Props = { secretPath: string; projectSlug: string; environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const ElasticSearchInputForm = ({ @@ -93,7 +94,8 @@ export const ElasticSearchInputForm = ({ onCancel, environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -111,7 +113,7 @@ export const ElasticSearchInputForm = ({ roles: ["superuser"], port: 443 }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -416,7 +418,7 @@ export const ElasticSearchInputForm = ({ )} /> - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -118,7 +120,7 @@ export const LdapInputForm = ({ rollbackLdif: "", credentialType: CredentialType.Dynamic }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -386,7 +388,7 @@ export const LdapInputForm = ({ )} /> )} - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -112,7 +114,7 @@ export const MongoAtlasInputForm = ({ provider: { roles: [{ databaseName: "", roleName: "" }] }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -448,7 +450,7 @@ export const MongoAtlasInputForm = ({ - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -87,7 +89,7 @@ export const MongoDBDatabaseInputForm = ({ provider: { roles: [{ roleName: "readWrite" }] }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -338,7 +340,7 @@ export const MongoDBDatabaseInputForm = ({ )} /> - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -100,7 +102,7 @@ export const RabbitMqInputForm = ({ }, tags: [] }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -422,7 +424,7 @@ export const RabbitMqInputForm = ({ )} /> - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -85,7 +87,7 @@ export const RedisInputForm = ({ creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all", revocationStatement: "ACL DELUSER {{username}}" }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -323,7 +325,7 @@ export const RedisInputForm = ({ - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -86,7 +88,7 @@ sp_role 'grant', 'mon_role', '{{username}}';`, revocationStatement: `sp_dropuser '{{username}}'; sp_droplogin '{{username}}';` }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -301,7 +303,7 @@ sp_droplogin '{{username}}';` - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -86,7 +88,7 @@ GRANT "MONITORING" TO {{username}};`, DROP USER {{username}};`, renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';" }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -321,7 +323,7 @@ DROP USER {{username}};`, - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -89,7 +91,7 @@ export const SnowflakeInputForm = ({ revocationStatement: "DROP USER {{username}};", renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};" }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -324,7 +326,7 @@ export const SnowflakeInputForm = ({ - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { @@ -150,7 +151,8 @@ export const SqlDatabaseInputForm = ({ onCancel, environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { currentWorkspace } = useWorkspace(); @@ -176,7 +178,7 @@ export const SqlDatabaseInputForm = ({ allowedSymbols: "-_.~!*" } }, - environment: environments.length === 1 ? environments[0] : undefined + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -660,7 +662,7 @@ export const SqlDatabaseInputForm = ({ - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( { const { control, @@ -92,7 +94,7 @@ export const TotpInputForm = ({ provider: { configType: ConfigType.URL }, - environment: environments.length === 1 ? environments[0] : environments[0] + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -305,7 +307,7 @@ export const TotpInputForm = ({

)} - {environments.length > 1 && ( + {!isSingleEnvironmentMode && ( Date: Mon, 17 Mar 2025 21:28:50 +0530 Subject: [PATCH 14/18] fix: corrected table name check in migration --- backend/src/db/migrations/20250212191958_create-gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20250212191958_create-gateway.ts b/backend/src/db/migrations/20250212191958_create-gateway.ts index 7738da5cc..14c498ca9 100644 --- a/backend/src/db/migrations/20250212191958_create-gateway.ts +++ b/backend/src/db/migrations/20250212191958_create-gateway.ts @@ -85,7 +85,7 @@ export async function up(knex: Knex): Promise { } if (await knex.schema.hasTable(TableName.DynamicSecret)) { - const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"); + const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId"); await knex.schema.alterTable(TableName.DynamicSecret, (t) => { // not setting a foreign constraint so that cascade effects are not triggered if (!doesGatewayColExist) { From 18c32d872ce821abc3880e87f7311e32530a5955 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 17 Mar 2025 12:06:35 -0400 Subject: [PATCH 15/18] Revert "feat: confirm environment exists when running `run` command" --- .gitignore | 8 ++--- cli/packages/api/model.go | 14 +++----- cli/packages/cmd/run.go | 69 +++--------------------------------- cli/packages/util/secrets.go | 1 + cli/test/secrets_test.go | 1 + flake.nix | 10 ------ 6 files changed, 14 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index f21c85c41..f2a23324b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -.direnv/ - # backend node_modules .env @@ -28,6 +26,8 @@ node_modules /.pnp .pnp.js +.env + # testing coverage reports @@ -63,12 +63,10 @@ yarn-error.log* # Editor specific .vscode/* -**/.idea/* +.idea/* frontend-build -# cli -.go/ *.tgz cli/infisical-merge cli/test/infisical-merge diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 1eb936f24..72dbbc97b 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -2,12 +2,6 @@ package api import "time" -type Environment struct { - Name string `json:"name"` - Slug string `json:"slug"` - ID string `json:"id"` -} - // Stores info for login one type LoginOneRequest struct { Email string `json:"email"` @@ -20,6 +14,7 @@ type LoginOneResponse struct { } // Stores info for login two + type LoginTwoRequest struct { Email string `json:"email"` ClientProof string `json:"clientProof"` @@ -173,10 +168,9 @@ type Secret struct { } type Project struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Environments []Environment `json:"environments"` + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` } type RawSecret struct { diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 388a4861d..7f11a3f95 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -15,9 +15,6 @@ import ( "syscall" "time" - "github.com/Infisical/infisical-merge/packages/api" - "github.com/go-resty/resty/v2" - "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" @@ -62,11 +59,11 @@ var runCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - environmentSlug, _ := cmd.Flags().GetString("env") + environmentName, _ := cmd.Flags().GetString("env") if !cmd.Flags().Changed("env") { environmentFromWorkspace := util.GetEnvFromWorkspaceFile() if environmentFromWorkspace != "" { - environmentSlug = environmentFromWorkspace + environmentName = environmentFromWorkspace } } @@ -139,20 +136,8 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - log.Debug().Msgf("Confirming selected environment is valid: %s", environmentSlug) - - hasEnvironment, err := confirmProjectHasEnvironment(environmentSlug, projectId, token) - if err != nil { - util.HandleError(err, "Could not confirm project has environment") - } - if !hasEnvironment { - util.HandleError(fmt.Errorf("project does not have environment '%s'", environmentSlug)) - } - - log.Debug().Msgf("Project '%s' has environment '%s'", projectId, environmentSlug) - request := models.GetAllSecretsParameters{ - Environment: environmentSlug, + Environment: environmentName, WorkspaceId: projectId, TagSlugs: tagSlugs, SecretsPath: secretsPath, @@ -323,6 +308,7 @@ func waitForExitCommand(cmd *exec.Cmd) (int, error) { } func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) { + var cmd *exec.Cmd var err error var lastSecretsFetch time.Time @@ -453,53 +439,8 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt } } -func confirmProjectHasEnvironment(environmentSlug, projectId string, token *models.TokenDetails) (bool, error) { - var accessToken string - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - accessToken = token.Token - } else { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") - } - accessToken = loggedInUserDetails.UserCredentials.JTWToken - } - - if projectId == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.HandleError(err, "Unable to get local project details") - } - - projectId = workspaceFile.WorkspaceId - } - - httpClient := resty.New() - httpClient.SetAuthToken(accessToken). - SetHeader("Accept", "application/json") - - project, err := api.CallGetProjectById(httpClient, projectId) - if err != nil { - return false, err - } - - for _, env := range project.Environments { - if env.Slug == environmentSlug { - return true, nil - } - } - return false, nil -} - func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) { + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { request.InfisicalToken = token.Token } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 0e26e03b0..5a2a0ec24 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -232,6 +232,7 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) { var secretsToReturn []models.SingleEnvironmentVariable + // var serviceTokenDetails api.GetServiceTokenDetailsResponse var errorToReturn error if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" { diff --git a/cli/test/secrets_test.go b/cli/test/secrets_test.go index 642d33277..f7f0f13ff 100644 --- a/cli/test/secrets_test.go +++ b/cli/test/secrets_test.go @@ -76,6 +76,7 @@ func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) { if err != nil { t.Fatalf("snapshot failed: %v", err) } + } func TestUserAuth_SecretsGetAll(t *testing.T) { diff --git a/flake.nix b/flake.nix index cc4cf8d65..094cfedc6 100644 --- a/flake.nix +++ b/flake.nix @@ -14,21 +14,11 @@ git lazygit - go python312Full nodejs_20 nodePackages.prettier infisical ]; - - env = { - GOROOT = "${pkgs.go}/share/go"; - }; - - shellHook = '' - export GOPATH="$(pwd)/.go" - mkdir -p "$GOPATH" - ''; }; }; } From c4da1ce32dc7a1007fa097a34635e0f1f80c3947 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 17 Mar 2025 23:38:24 +0530 Subject: [PATCH 16/18] feat: resolved PR feedbacks --- .../server/plugins/auth/inject-identity.ts | 10 +++-- .../routes/v1/identity-oidc-auth-router.ts | 4 +- .../identity-oidc-auth-types.ts | 2 +- frontend/src/hooks/api/identities/types.ts | 4 +- .../IdentitySection/IdentityOidcAuthForm.tsx | 38 ++++++++++--------- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 9005fbf96..5779b39f7 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -138,10 +138,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { identityName: identity.name, authMethod: null }; - requestContext.set("identityAuthInfo", { - identityId: identity.identityId, - oidc: token?.identityAuth?.oidc - }); + if (token?.identityAuth?.oidc) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + oidc: token?.identityAuth?.oidc + }); + } break; } case AuthMode.SERVICE_TOKEN: { diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index a27c12daf..f3098c851 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -105,7 +105,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer), boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences), boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims), - claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.claimMetadataMapping), + claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.claimMetadataMapping).optional(), boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject), accessTokenTrustedIps: z .object({ @@ -203,7 +203,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer), boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences), boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims), - claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping), + claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping).optional(), boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject), accessTokenTrustedIps: z .object({ diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts index 7f9f62296..9727285e7 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts @@ -7,7 +7,7 @@ export type TAttachOidcAuthDTO = { boundIssuer: string; boundAudiences: string; boundClaims: Record; - claimMetadataMapping: Record; + claimMetadataMapping?: Record; boundSubject: string; accessTokenTTL: number; accessTokenMaxTTL: number; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 9920fcf0b..86d35e086 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -193,7 +193,7 @@ export type IdentityOidcAuth = { boundIssuer: string; boundAudiences: string; boundClaims: Record; - claimMetadataMapping: Record; + claimMetadataMapping?: Record; boundSubject: string; accessTokenTTL: number; accessTokenMaxTTL: number; @@ -209,7 +209,7 @@ export type AddIdentityOidcAuthDTO = { boundIssuer: string; boundAudiences: string; boundClaims: Record; - claimMetadataMapping: Record; + claimMetadataMapping?: Record; boundSubject: string; accessTokenTTL: number; accessTokenMaxTTL: number; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx index 094be734e..21440457a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -52,12 +52,15 @@ const schema = z.object({ value: z.string() }) ), - claimMetadataMapping: z.array( - z.object({ - key: z.string(), - value: z.string() - }) - ), + claimMetadataMapping: z + .array( + z.object({ + key: z.string(), + value: z.string() + }) + ) + .optional() + .nullable(), boundSubject: z.string().optional().default("") }); @@ -105,7 +108,6 @@ export const IdentityOidcAuthForm = ({ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] } }); - const { fields: boundClaimsFields, append: appendBoundClaimField, @@ -141,10 +143,12 @@ export const IdentityOidcAuthForm = ({ key, value })), - claimMetadataMapping: Object.entries(data.claimMetadataMapping).map(([key, value]) => ({ - key, - value - })), + claimMetadataMapping: data?.claimMetadataMapping + ? Object.entries(data.claimMetadataMapping).map(([key, value]) => ({ + key, + value + })) + : undefined, boundSubject: data.boundSubject, accessTokenTTL: String(data.accessTokenTTL), accessTokenMaxTTL: String(data.accessTokenMaxTTL), @@ -200,9 +204,9 @@ export const IdentityOidcAuthForm = ({ boundIssuer, boundAudiences, boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), - claimMetadataMapping: Object.fromEntries( - claimMetadataMapping.map((entry) => [entry.key, entry.value]) - ), + claimMetadataMapping: claimMetadataMapping + ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value])) + : undefined, boundSubject, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), @@ -217,9 +221,9 @@ export const IdentityOidcAuthForm = ({ boundIssuer, boundAudiences, boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), - claimMetadataMapping: Object.fromEntries( - claimMetadataMapping.map((entry) => [entry.key, entry.value]) - ), + claimMetadataMapping: claimMetadataMapping + ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value])) + : undefined, boundSubject, organizationId: orgId, accessTokenTTL: Number(accessTokenTTL), From 9eb89bb46d93e49456213eed5406a8bce3e36b89 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 17 Mar 2025 23:52:11 +0530 Subject: [PATCH 17/18] fix: null causing ui error --- .../ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx index 3eedf1411..116ecfd38 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx @@ -112,7 +112,7 @@ export const ViewIdentityOidcAuthContent = ({ )} - {Object.keys(data.claimMetadataMapping).length && ( + {data.claimMetadataMapping && Object.keys(data.claimMetadataMapping).length && ( Date: Mon, 17 Mar 2025 15:10:36 -0700 Subject: [PATCH 18/18] improvements: address feedback --- docs/integrations/secret-syncs/aws-parameter-store.mdx | 4 +++- docs/integrations/secret-syncs/aws-secrets-manager.mdx | 4 +++- .../forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx | 4 ++-- .../SecretSyncOptionsSection/SecretSyncOptionsSection.tsx | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 831d0c752..fad37265a 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -43,7 +43,9 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with. - **Tags**: Optional resource tags to add to parameters synced by Infisical. - **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical. - Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. + + Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. + - **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. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index a301ee86b..8ed85be25 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -46,7 +46,9 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with. - **Tags**: Optional tags to add to secrets synced by Infisical. - **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical. - Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. + + Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. + - **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. diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 6982d3b77..040944298 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -113,8 +113,8 @@ export const SecretSyncReviewFields = () => { {appendSuffix} */} {AdditionalSyncOptionsFieldsComponent} {disableSecretDeletion && ( - - Enabled + + Disabled )} diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 139fcf916..351cb70e1 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -85,8 +85,8 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = {appendSuffix} */} {AdditionalSyncOptionsComponent} {disableSecretDeletion && ( - - Enabled + + Disabled )}