Merge branch 'main' into ENG-2647

This commit is contained in:
x032205
2025-05-05 15:27:25 -04:00
83 changed files with 4593 additions and 868 deletions

View File

@@ -0,0 +1,27 @@
name: Release Gateway Helm Chart
on:
workflow_dispatch:
jobs:
release-helm:
name: Release Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: v3.10.0
- name: Install python
uses: actions/setup-python@v4
- name: Install Cloudsmith CLI
run: pip install --upgrade cloudsmith-cli
- name: Build and push helm package to CloudSmith
run: cd helm-charts && sh upload-gateway-cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}

View File

@@ -69,6 +69,15 @@ module.exports = {
["^\\."]
]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"": "never", // this is required to get the .tsx to work...
ts: "never",
tsx: "never"
}
]
}
};

2493
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,8 @@
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest",
"email:dev": "email dev --dir src/services/smtp/emails"
},
"keywords": [],
"author": "",
@@ -96,6 +97,7 @@
"@types/picomatch": "^2.3.3",
"@types/pkcs11js": "^1.0.4",
"@types/prompt-sync": "^4.2.3",
"@types/react": "^19.1.2",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/sjcl": "^1.0.34",
@@ -115,6 +117,7 @@
"nodemon": "^3.0.2",
"pino-pretty": "^10.2.3",
"prompt-sync": "^4.2.0",
"react-email": "4.0.7",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.8",
@@ -164,6 +167,7 @@
"@opentelemetry/semantic-conventions": "^1.27.0",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.12.1",
"@react-email/components": "0.0.36",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.2",
@@ -223,6 +227,8 @@
"posthog-node": "^3.6.2",
"probot": "^13.3.8",
"re2": "^1.21.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",

View File

@@ -219,7 +219,7 @@ export const parseRotationErrorMessage = (err: unknown): string => {
if (err instanceof AxiosError) {
errorMessage += err?.response?.data
? JSON.stringify(err?.response?.data)
: err?.message ?? "An unknown error occurred.";
: (err?.message ?? "An unknown error occurred.");
} else {
errorMessage += (err as Error)?.message || "An unknown error occurred.";
}

View File

@@ -282,7 +282,7 @@ export const sshCertificateAuthorityServiceFactory = ({
// set [keyId] depending on if [allowCustomKeyIds] is true or false
const keyId = sshCertificateTemplate.allowCustomKeyIds
? requestedKeyId ?? `${actor}-${actorId}`
? (requestedKeyId ?? `${actor}-${actorId}`)
: `${actor}-${actorId}`;
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
@@ -404,7 +404,7 @@ export const sshCertificateAuthorityServiceFactory = ({
// set [keyId] depending on if [allowCustomKeyIds] is true or false
const keyId = sshCertificateTemplate.allowCustomKeyIds
? requestedKeyId ?? `${actor}-${actorId}`
? (requestedKeyId ?? `${actor}-${actorId}`)
: `${actor}-${actorId}`;
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });

View File

@@ -401,8 +401,8 @@ export const authLoginServiceFactory = ({
}
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
const mfaMethod = orgMfaMethod ?? userMfaMethod;
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
@@ -573,9 +573,9 @@ export const authLoginServiceFactory = ({
}: TVerifyMfaTokenDTO) => {
const appCfg = getConfig();
const user = await userDAL.findById(userId);
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
try {
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
if (mfaMethod === MfaMethod.EMAIL) {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,

View File

@@ -698,6 +698,8 @@ export const orgServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const invitingUser = await userDAL.findOne({ id: actorId });
const org = await orgDAL.findOrgById(orgId);
const [inviteeOrgMembership] = await orgDAL.findMembership({
@@ -731,8 +733,8 @@ export const orgServiceFactory = ({
subjectLine: "Infisical organization invitation",
recipients: [inviteeOrgMembership.email as string],
substitutions: {
inviterFirstName: inviteeOrgMembership.firstName,
inviterUsername: inviteeOrgMembership.email,
inviterFirstName: invitingUser.firstName,
inviterUsername: invitingUser.email,
organizationName: org?.name,
email: inviteeOrgMembership.email,
organizationId: org?.id.toString(),
@@ -761,6 +763,8 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const invitingUser = await userDAL.findOne({ id: actorId });
const org = await orgDAL.findOrgById(orgId);
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
@@ -1179,8 +1183,8 @@ export const orgServiceFactory = ({
subjectLine: "Infisical organization invitation",
recipients: [el.email],
substitutions: {
inviterFirstName: el.firstName,
inviterUsername: el.email,
inviterFirstName: invitingUser.firstName,
inviterUsername: invitingUser.email,
organizationName: org?.name,
email: el.email,
organizationId: org?.id.toString(),

View File

@@ -291,7 +291,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
} else if (err instanceof AxiosError) {
errorMessage = err?.response?.data
? JSON.stringify(err?.response?.data)
: err?.message ?? "An unknown error occurred.";
: (err?.message ?? "An unknown error occurred.");
} else {
errorMessage = (err as Error)?.message || "An unknown error occurred.";
}

View File

@@ -834,7 +834,7 @@ export const secretSyncQueueFactory = ({
secretPath: folder?.path,
environment: environment?.name,
projectName: project.name,
syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
syncUrl: `${appCfg.SITE_URL}/secret-manager/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
}
});
};

View File

@@ -740,7 +740,7 @@ export const secretQueueFactory = ({
environment: jobPayload.environmentName,
count: jobPayload.count,
projectName: project.name,
integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}`
integrationUrl: `${appCfg.SITE_URL}/secret-manager/${project.id}/integrations?selectedTab=native-integrations`
}
});
}

View File

@@ -0,0 +1,95 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface AccessApprovalRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
projectName: string;
requesterFullName: string;
requesterEmail: string;
isTemporary: boolean;
secretPath: string;
environment: string;
expiresIn: string;
permissions: string[];
note?: string;
approvalUrl: string;
}
export const AccessApprovalRequestTemplate = ({
projectName,
siteUrl,
requesterFullName,
requesterEmail,
isTemporary,
secretPath,
environment,
expiresIn,
permissions,
note,
approvalUrl
}: AccessApprovalRequestTemplateProps) => {
return (
<BaseEmailWrapper
title="Access Approval Request"
preview="A new access approval request is pending your review."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
You have a new access approval request pending review for the project <strong>{projectName}</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
<strong>{requesterFullName}</strong> (
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
{requesterEmail}
</Link>
) has requested {isTemporary ? "temporary" : "permanent"} access to <strong>{secretPath}</strong> in the{" "}
<strong>{environment}</strong> environment.
</Text>
{isTemporary && (
<Text className="text-[14px] text-red-500 leading-[24px]">
<strong>This access will expire {expiresIn} after approval.</strong>
</Text>
)}
<Text className="text-[14px] leading-[24px] mb-[4px]">
<strong>The following permissions are requested:</strong>
</Text>
{permissions.map((permission) => (
<Text key={permission} className="text-[14px] my-[2px] leading-[24px]">
- {permission}
</Text>
))}
{note && (
<Text className="text-[14px] text-slate-700 leading-[24px]">
<strong className="text-black">User Note:</strong> "{note}"
</Text>
)}
</Section>
<Section className="text-center mt-[28px]">
<Button
href={approvalUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Review Request
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default AccessApprovalRequestTemplate;
AccessApprovalRequestTemplate.PreviewProps = {
requesterFullName: "Abigail Williams",
requesterEmail: "abigail@infisical.com",
isTemporary: true,
secretPath: "/api/secrets",
environment: "Production",
siteUrl: "https://infisical.com",
projectName: "Example Project",
expiresIn: "1 day",
permissions: ["Read Secret", "Delete Project", "Create Dynamic Secret"],
note: "I need access to these permissions for the new initiative for HR."
} as AccessApprovalRequestTemplateProps;

View File

@@ -0,0 +1,45 @@
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Tailwind, Text } from "@react-email/components";
import React, { ReactNode } from "react";
export interface BaseEmailWrapperProps {
title: string;
preview: string;
siteUrl: string;
children?: ReactNode;
}
export const BaseEmailWrapper = ({ title, preview, children, siteUrl }: BaseEmailWrapperProps) => {
return (
<Html>
<Head title={title} />
<Tailwind>
<Body className="bg-gray-300 my-auto mx-auto font-sans px-[8px]">
<Preview>{preview}</Preview>
<Container className="bg-white rounded-xl my-[40px] mx-auto pb-[0px] max-w-[500px]">
<Section className="border-0 border-b border-[#d1e309] border-solid bg-[#EBF852] mb-[44px] h-[10px] rounded-t-xl" />
<Section className="px-[32px] mb-[18px]">
<Section className="w-[48px] h-[48px] border border-solid border-gray-300 rounded-full bg-gray-100 mx-auto">
<Img
src={`https://infisical.com/_next/image?url=%2Fimages%2Flogo-black.png&w=64&q=75`}
width="32"
alt="Infisical Logo"
className="mx-auto"
/>
</Section>
</Section>
<Section className="px-[28px]">{children}</Section>
<Hr className=" mt-[32px] mb-[0px] h-[1px]" />
<Section className="px-[24px] text-center">
<Text className="text-gray-500 text-[12px]">
Email sent via{" "}
<Link href={siteUrl} className="text-slate-700 no-underline">
Infisical
</Link>
</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};

View File

@@ -0,0 +1,50 @@
import { Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface EmailMfaTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
code: string;
isCloud: boolean;
}
export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplateProps) => {
return (
<BaseEmailWrapper title="MFA Code" preview="Sign-in attempt requires further verification." siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>MFA required</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[8px] text-center pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text>Enter the MFA code shown below in the browser where you started sign-in.</Text>
<Text className="text-[24px] mt-[16px]">
<strong>{code}</strong>
</Text>
</Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]">
<strong>Not you?</strong>{" "}
{isCloud ? (
<>
Contact us at{" "}
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
support@infisical.com
</Link>{" "}
immediately
</>
) : (
"Contact your administrator immediately"
)}
.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default EmailMfaTemplate;
EmailMfaTemplate.PreviewProps = {
code: "124356",
isCloud: true,
siteUrl: "https://infisical.com"
} as EmailMfaTemplateProps;

View File

@@ -0,0 +1,53 @@
import { Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface EmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
code: string;
isCloud: boolean;
}
export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerificationTemplateProps) => {
return (
<BaseEmailWrapper
title="Confirm Your Email Address"
preview="Verify your email address to continue with Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Confirm your email address</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[8px] text-center pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text>Enter the confirmation code shown below in the browser window requiring confirmation.</Text>
<Text className="text-[24px] mt-[16px]">
<strong>{code}</strong>
</Text>
</Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]">
<strong>Questions about Infisical?</strong>{" "}
{isCloud ? (
<>
Email us at{" "}
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
support@infisical.com
</Link>
</>
) : (
"Contact your administrator"
)}
.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default EmailVerificationTemplate;
EmailVerificationTemplate.PreviewProps = {
code: "124356",
isCloud: true,
siteUrl: "https://infisical.com"
} as EmailVerificationTemplateProps;

View File

@@ -0,0 +1,43 @@
import { Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ExternalImportFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
error: string;
provider: string;
}
export const ExternalImportFailedTemplate = ({ error, siteUrl, provider }: ExternalImportFailedTemplateProps) => {
return (
<BaseEmailWrapper title="Import Failed" preview={`An import from ${provider} has failed.`} siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
An import from <strong>{provider}</strong> to Infisical has failed
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
An import from <strong>{provider}</strong> to Infisical has failed due to unforeseen circumstances. Please
re-try your import.
</Text>
<Text className="text-black text-[14px] leading-[24px]">
If your issue persists, you can contact the Infisical team at{" "}
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
support@infisical.com
</Link>
.
</Text>
<Text className="text-[14px] text-red-500 leading-[24px]">
<strong>Error:</strong> "{error}"
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default ExternalImportFailedTemplate;
ExternalImportFailedTemplate.PreviewProps = {
provider: "EnvKey",
error: "Something went wrong. Please try again.",
siteUrl: "https://infisical.com"
} as ExternalImportFailedTemplateProps;

View File

@@ -0,0 +1,31 @@
import { Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ExternalImportStartedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
provider: string;
}
export const ExternalImportStartedTemplate = ({ siteUrl, provider }: ExternalImportStartedTemplateProps) => {
return (
<BaseEmailWrapper title="Import in Progress" preview={`An import from ${provider} has started.`} siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
An import from <strong>{provider}</strong> to Infisical has been started
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
An import from <strong>{provider}</strong> to Infisical is in progress. The import process may take up to 30
minutes. You will receive an email once the import has completed.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default ExternalImportStartedTemplate;
ExternalImportStartedTemplate.PreviewProps = {
provider: "EnvKey",
siteUrl: "https://infisical.com"
} as ExternalImportStartedTemplateProps;

View File

@@ -0,0 +1,31 @@
import { Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ExternalImportSucceededTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
provider: string;
}
export const ExternalImportSucceededTemplate = ({ siteUrl, provider }: ExternalImportSucceededTemplateProps) => {
return (
<BaseEmailWrapper title="Import Complete" preview={`An import from ${provider} has completed.`} siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
An import from <strong>{provider}</strong> to Infisical has completed
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
An import from <strong>{provider}</strong> to Infisical was successful. Your data is now available in
Infisical.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default ExternalImportSucceededTemplate;
ExternalImportSucceededTemplate.PreviewProps = {
provider: "EnvKey",
siteUrl: "https://infisical.com"
} as ExternalImportSucceededTemplateProps;

View File

@@ -0,0 +1,65 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface IntegrationSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
count: number;
projectName: string;
secretPath: string;
environment: string;
syncMessage: string;
integrationUrl: string;
}
export const IntegrationSyncFailedTemplate = ({
count,
siteUrl,
projectName,
secretPath,
environment,
syncMessage,
integrationUrl
}: IntegrationSyncFailedTemplateProps) => {
return (
<BaseEmailWrapper
title="Integration Sync Failed"
preview="An integration sync error has occurred."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>{count}</strong> integration(s) failed to sync
</Heading>
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<strong>Project</strong>
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
<strong>Environment</strong>
<Text className="text-[14px] mt-[4px]">{environment}</Text>
<strong>Secret Path</strong>
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
<strong className="text-black">Failure Reason:</strong>
<Text className="text-[14px] mt-[4px] text-red-500 leading-[24px]">"{syncMessage}"</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={integrationUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
View Integrations
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default IntegrationSyncFailedTemplate;
IntegrationSyncFailedTemplate.PreviewProps = {
projectName: "Example Project",
secretPath: "/api/secrets",
environment: "Production",
siteUrl: "https://infisical.com",
integrationUrl: "https://infisical.com",
count: 2,
syncMessage: "Secret key cannot contain a colon (:)"
} as IntegrationSyncFailedTemplateProps;

View File

@@ -0,0 +1,68 @@
import { Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface NewDeviceLoginTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string;
timestamp: string;
ip: string;
userAgent: string;
isCloud: boolean;
}
export const NewDeviceLoginTemplate = ({
email,
timestamp,
ip,
userAgent,
siteUrl,
isCloud
}: NewDeviceLoginTemplateProps) => {
return (
<BaseEmailWrapper
title="Successful Login from New Device"
preview="New device login from Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
We're verifying a recent login for
<br />
<strong>{email}</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<strong>Timestamp</strong>
<Text className="text-[14px] mt-[4px]">{timestamp}</Text>
<strong>IP Address</strong>
<Text className="text-[14px] mt-[4px]">{ip}</Text>
<strong>User Agent</strong>
<Text className="text-[14px] mt-[4px]">{userAgent}</Text>
</Section>
<Section className="mt-[24px] bg-gray-50 px-[24px] pt-[2px] pb-[16px] border border-solid border-gray-200 rounded-md text-gray-800">
<Text className="mb-[0px]">
If you believe that this login is suspicious, please contact{" "}
{isCloud ? (
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
support@infisical.com
</Link>
) : (
"your administrator"
)}{" "}
or reset your password immediately.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default NewDeviceLoginTemplate;
NewDeviceLoginTemplate.PreviewProps = {
email: "john@infisical.com",
ip: "127.0.0.1",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15",
timestamp: "Tue Apr 29 2025 23:03:27 GMT+0000 (Coordinated Universal Time)",
isCloud: true,
siteUrl: "https://infisical.com"
} as NewDeviceLoginTemplateProps;

View File

@@ -0,0 +1,57 @@
import { Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface OrgAdminBreakglassAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string;
timestamp: string;
ip: string;
userAgent: string;
}
export const OrgAdminBreakglassAccessTemplate = ({
email,
siteUrl,
timestamp,
ip,
userAgent
}: OrgAdminBreakglassAccessTemplateProps) => {
return (
<BaseEmailWrapper
title="Organization Admin has Bypassed SSO"
preview="An organization admin has bypassed SSO."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
The organization admin <strong>{email}</strong> has bypassed enforced SSO login
</Heading>
<Section className="px-[24px] mt-[36px] pt-[24px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<strong className="text-[14px]">Timestamp</strong>
<Text className="text-[14px] mt-[4px]">{timestamp}</Text>
<strong className="text-[14px]">IP Address</strong>
<Text className="text-[14px] mt-[4px]">{ip}</Text>
<strong className="text-[14px]">User Agent</strong>
<Text className="text-[14px] mt-[4px]">{userAgent}</Text>
<Text className="text-[14px]">
If you'd like to disable Admin SSO Bypass, please visit{" "}
<Link href={`${siteUrl}/organization/settings`} className="text-slate-700 no-underline">
Organization Security Settings
</Link>
.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default OrgAdminBreakglassAccessTemplate;
OrgAdminBreakglassAccessTemplate.PreviewProps = {
ip: "127.0.0.1",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15",
timestamp: "Tue Apr 29 2025 23:03:27 GMT+0000 (Coordinated Universal Time)",
siteUrl: "https://infisical.com",
email: "august@infisical.com"
} as OrgAdminBreakglassAccessTemplateProps;

View File

@@ -0,0 +1,41 @@
import { Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface OrgAdminProjectGrantAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview"> {
email: string;
projectName: string;
}
export const OrgAdminProjectGrantAccessTemplate = ({
email,
siteUrl,
projectName
}: OrgAdminProjectGrantAccessTemplateProps) => {
return (
<BaseEmailWrapper
title="Project Access Granted to Organization Admin"
preview="An organization admin has self-issued direct access to a project in Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
An organization admin has joined the project <strong>{projectName}</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[24px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px] mt-[4px]">
The organization admin <strong>{email}</strong> has self-issued direct access to the project{" "}
<strong>{projectName}</strong>.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default OrgAdminProjectGrantAccessTemplate;
OrgAdminProjectGrantAccessTemplate.PreviewProps = {
email: "kevin@infisical.com",
projectName: "Example Project",
siteUrl: "https://infisical.com"
} as OrgAdminProjectGrantAccessTemplateProps;

View File

@@ -0,0 +1,77 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
metadata?: string;
inviterFirstName: string;
inviterUsername: string;
organizationName: string;
email: string;
organizationId: string;
token: string;
callback_url: string;
}
export const OrganizationInvitationTemplate = ({
organizationName,
inviterFirstName,
inviterUsername,
token,
callback_url,
metadata,
email,
organizationId,
siteUrl
}: OrganizationInvitationTemplateProps) => {
return (
<BaseEmailWrapper
title="Organization Invitation"
preview="You've been invited to join an organization on Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
You've been invited to join
<br />
<strong>{organizationName}</strong> on <strong>Infisical</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
<strong>{inviterFirstName}</strong> (
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline">
{inviterUsername}
</Link>
) has invited you to collaborate on <strong>{organizationName}</strong>.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={`${callback_url}?token=${token}${metadata ? `&metadata=${metadata}` : ""}&to=${encodeURIComponent(email)}&organization_id=${organizationId}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Accept Invite
</Button>
</Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]">
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
certificates, SSH keys, and configurations across your team and infrastructure.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default OrganizationInvitationTemplate;
OrganizationInvitationTemplate.PreviewProps = {
organizationName: "Example Organization",
inviterFirstName: "Jane",
inviterUsername: "jane@infisical.com",
email: "john@infisical.com",
siteUrl: "https://infisical.com",
callback_url: "https://app.infisical.com",
token: "preview-token",
organizationId: "1ae1c2c7-8068-461c-b15e-421737868a6a"
} as OrganizationInvitationTemplateProps;

View File

@@ -0,0 +1,60 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface PasswordResetTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string;
callback_url: string;
token: string;
isCloud: boolean;
}
export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, token }: PasswordResetTemplateProps) => {
return (
<BaseEmailWrapper
title="Account Recovery"
preview="A password reset was requested for your Infisical account."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Account Recovery</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">A password reset was requested for your Infisical account.</Text>
<Text className="text-[14px]">
If you did not initiate this request, please contact{" "}
{isCloud ? (
<>
us immediately at{" "}
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
support@infisical.com
</Link>
</>
) : (
"your administrator immediately"
)}
.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Reset Password
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default PasswordResetTemplate;
PasswordResetTemplate.PreviewProps = {
email: "kevin@infisical.com",
callback_url: "https://app.infisical.com",
isCloud: true,
token: "preview-token",
siteUrl: "https://infisical.com"
} as PasswordResetTemplateProps;

View File

@@ -0,0 +1,59 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface PasswordSetupTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string;
callback_url: string;
token: string;
isCloud: boolean;
}
export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, token }: PasswordSetupTemplateProps) => {
return (
<BaseEmailWrapper title="Password Setup" preview="Setup your password for Infisical." siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Password Setup</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">Someone requested to set up a password for your Infisical account.</Text>
<Text className="text-[14px] text-red-500">
Make sure you are already logged in to Infisical in the current browser before clicking the link below.
</Text>
<Text className="text-[14px]">
If you did not initiate this request, please contact{" "}
{isCloud ? (
<>
us immediately at{" "}
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
support@infisical.com
</Link>
</>
) : (
"your administrator immediately"
)}
.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Set Up Password
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default PasswordSetupTemplate;
PasswordSetupTemplate.PreviewProps = {
email: "casey@infisical.com",
callback_url: "https://app.infisical.com",
isCloud: true,
siteUrl: "https://infisical.com",
token: "preview-token"
} as PasswordSetupTemplateProps;

View File

@@ -0,0 +1,69 @@
import { Heading, Hr, Section, Text } from "@react-email/components";
import React, { Fragment } from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface PkiExpirationAlertTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
alertName: string;
alertBeforeDays: number;
items: { type: string; friendlyName: string; serialNumber: string; expiryDate: string }[];
}
export const PkiExpirationAlertTemplate = ({
alertName,
siteUrl,
alertBeforeDays,
items
}: PkiExpirationAlertTemplateProps) => {
return (
<BaseEmailWrapper
title="Infisical CA/Certificate Expiration Notice"
preview="One or more of your Infisical certificates is about to expire."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>CA/Certificate Expiration Notice</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text>Hello,</Text>
<Text className="text-black text-[14px] leading-[24px]">
This is an automated alert for <strong>{alertName}</strong> triggered for CAs/Certificates expiring in{" "}
<strong>{alertBeforeDays}</strong> days.
</Text>
<Text className="text-[14px] leading-[24px] mb-[4px]">
<strong>Expiring Items:</strong>
</Text>
{items.map((item) => (
<Fragment key={item.serialNumber}>
<Hr className="mb-[16px]" />
<strong className="text-[14px]">{item.type}:</strong>
<Text className="text-[14px] my-[2px] leading-[24px]">{item.friendlyName}</Text>
<strong className="text-[14px]">Serial Number:</strong>
<Text className="text-[14px] my-[2px] leading-[24px]">{item.serialNumber}</Text>
<strong className="text-[14px]">Expires On:</strong>
<Text className="text-[14px] mt-[2px] mb-[16px] leading-[24px]">{item.expiryDate}</Text>
</Fragment>
))}
<Hr />
<Text className="text-[14px] leading-[24px]">
Please take the necessary actions to renew these items before they expire.
</Text>
<Text className="text-[14px] leading-[24px]">
For more details, please log in to your Infisical account and check your PKI management section.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default PkiExpirationAlertTemplate;
PkiExpirationAlertTemplate.PreviewProps = {
alertBeforeDays: 5,
items: [
{ type: "CA", friendlyName: "Example CA", serialNumber: "1234567890", expiryDate: "2032-01-01" },
{ type: "Certificate", friendlyName: "Example Certificate", serialNumber: "2345678901", expiryDate: "2032-01-01" }
],
alertName: "My PKI Alert",
siteUrl: "https://infisical.com"
} as PkiExpirationAlertTemplateProps;

View File

@@ -0,0 +1,68 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ProjectAccessRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
projectName: string;
requesterName: string;
requesterEmail: string;
orgName: string;
note: string;
callback_url: string;
}
export const ProjectAccessRequestTemplate = ({
projectName,
siteUrl,
requesterName,
requesterEmail,
orgName,
note,
callback_url
}: ProjectAccessRequestTemplateProps) => {
return (
<BaseEmailWrapper
title="Project Access Request"
preview="A user has requested access to an Infisical project."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
A user has requested access to the project <strong>{projectName}</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
<strong>{requesterName}</strong> (
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
{requesterEmail}
</Link>
) has requested access to the project <strong>{projectName}</strong> in the organization{" "}
<strong>{orgName}</strong>.
</Text>
<Text className="text-[14px] text-slate-700 leading-[24px]">
<strong className="text-black">User note:</strong> "{note}"
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={callback_url}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Grant Access
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default ProjectAccessRequestTemplate;
ProjectAccessRequestTemplate.PreviewProps = {
requesterName: "Abigail Williams",
requesterEmail: "abigail@infisical.com",
orgName: "Example Org",
siteUrl: "https://infisical.com",
projectName: "Example Project",
note: "I need access to the project for the new initiative for HR.",
callback_url: "https://infisical.com"
} as ProjectAccessRequestTemplateProps;

View File

@@ -0,0 +1,50 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ProjectInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
callback_url: string;
workspaceName: string;
}
export const ProjectInvitationTemplate = ({ callback_url, workspaceName, siteUrl }: ProjectInvitationTemplateProps) => {
return (
<BaseEmailWrapper
title="Project Invitation"
preview="You've been invited to join a project on Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
You've been invited to join a project on Infisical
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
You've been invited to join the project <strong>{workspaceName}</strong>.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={callback_url}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Join Project
</Button>
</Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]">
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
certificates, SSH keys, and configurations across your team and infrastructure.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default ProjectInvitationTemplate;
ProjectInvitationTemplate.PreviewProps = {
workspaceName: "Example Project",
siteUrl: "https://infisical.com",
callback_url: "https://app.infisical.com"
} as ProjectInvitationTemplateProps;

View File

@@ -0,0 +1,56 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ScimUserProvisionedTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
organizationName: string;
callback_url: string;
}
export const ScimUserProvisionedTemplate = ({
organizationName,
callback_url,
siteUrl
}: ScimUserProvisionedTemplateProps) => {
return (
<BaseEmailWrapper
title="Organization Invitation"
preview="You've been invited to join an organization on Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
You've been invited to join
<br />
<strong>{organizationName}</strong> on <strong>Infisical</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
You've been invited to collaborate on <strong>{organizationName}</strong>.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={callback_url}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Accept Invite
</Button>
</Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]">
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
certificates, SSH keys, and configurations across your team and infrastructure.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default ScimUserProvisionedTemplate;
ScimUserProvisionedTemplate.PreviewProps = {
organizationName: "Example Organization",
callback_url: "https://app.infisical.com",
siteUrl: "https://app.infisical.com"
} as ScimUserProvisionedTemplateProps;

View File

@@ -0,0 +1,72 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SecretApprovalRequestBypassedTemplateProps
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
projectName: string;
requesterFullName: string;
requesterEmail: string;
secretPath: string;
environment: string;
bypassReason: string;
approvalUrl: string;
}
export const SecretApprovalRequestBypassedTemplate = ({
projectName,
siteUrl,
requesterFullName,
requesterEmail,
secretPath,
environment,
bypassReason,
approvalUrl
}: SecretApprovalRequestBypassedTemplateProps) => {
return (
<BaseEmailWrapper
title="Secret Approval Request Bypassed"
preview="A secret approval request has been bypassed."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
A secret approval request has been bypassed in the project <strong>{projectName}</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
<strong>{requesterFullName}</strong> (
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
{requesterEmail}
</Link>
) has merged a secret to <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment
without obtaining the required approval.
</Text>
<Text className="text-[14px] text-slate-700 leading-[24px]">
<strong className="text-black">The following reason was provided for bypassing the policy:</strong> "
{bypassReason}"
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={approvalUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Review Bypass
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default SecretApprovalRequestBypassedTemplate;
SecretApprovalRequestBypassedTemplate.PreviewProps = {
requesterFullName: "Abigail Williams",
requesterEmail: "abigail@infisical.com",
secretPath: "/api/secrets",
environment: "Production",
siteUrl: "https://infisical.com",
projectName: "Example Project",
bypassReason: "I needed urgent access for a production misconfiguration."
} as SecretApprovalRequestBypassedTemplateProps;

View File

@@ -0,0 +1,57 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SecretApprovalRequestNeedsReviewTemplateProps
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
projectName: string;
firstName: string;
organizationName: string;
approvalUrl: string;
}
export const SecretApprovalRequestNeedsReviewTemplate = ({
projectName,
siteUrl,
firstName,
organizationName,
approvalUrl
}: SecretApprovalRequestNeedsReviewTemplateProps) => {
return (
<BaseEmailWrapper
title="Secret Change Approval Request"
preview="A secret change approval request requires review."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
A secret approval request for the project <strong>{projectName}</strong> requires review
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">Hello {firstName},</Text>
<Text className="text-black text-[14px] leading-[24px]">
You have a new secret change request pending your review for the project <strong>{projectName}</strong> in the
organization <strong>{organizationName}</strong>.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={approvalUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Review Changes
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default SecretApprovalRequestNeedsReviewTemplate;
SecretApprovalRequestNeedsReviewTemplate.PreviewProps = {
firstName: "Gordon",
organizationName: "Example Org",
siteUrl: "https://infisical.com",
approvalUrl: "https://infisical.com",
projectName: "Example Project"
} as SecretApprovalRequestNeedsReviewTemplateProps;

View File

@@ -0,0 +1,82 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SecretLeakIncidentTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
numberOfSecrets: number;
pusher_email: string;
pusher_name: string;
}
export const SecretLeakIncidentTemplate = ({
numberOfSecrets,
siteUrl,
pusher_name,
pusher_email
}: SecretLeakIncidentTemplateProps) => {
return (
<BaseEmailWrapper
title="Incident Alert: Secret(s) Leaked"
preview="Infisical uncovered one or more leaked secrets."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
Infisical has uncovered <strong>{numberOfSecrets}</strong> secret(s) from a recent commit
</Heading>
<Section className="px-[24px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">
You are receiving this notification because one or more leaked secrets have been detected in a recent commit
{(pusher_email || pusher_name) && (
<>
{" "}
pushed by <strong>{pusher_name ?? "Unknown Pusher"}</strong>{" "}
{pusher_email && (
<>
(
<Link href={`mailto:${pusher_email}`} className="text-slate-700 no-underline">
{pusher_email}
</Link>
)
</>
)}
</>
)}
.
</Text>
<Text className="text-[14px]">
If these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as
a comment in the given programming language. This will prevent future notifications from being sent out for
these secrets.
</Text>
<Text className="text-[14px] text-red-500">
If these are production secrets, please rotate them immediately.
</Text>
<Text className="text-[14px]">
Once you have taken action, be sure to update the status of the risk in the{" "}
<Link href={`${siteUrl}/organization/secret-scanning`} className="text-slate-700 no-underline">
Infisical Dashboard
</Link>
.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={`${siteUrl}/organization/secret-scanning`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
View Leaked Secrets
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default SecretLeakIncidentTemplate;
SecretLeakIncidentTemplate.PreviewProps = {
pusher_name: "Jim",
pusher_email: "jim@infisical.com",
numberOfSecrets: 3,
siteUrl: "https://infisical.com"
} as SecretLeakIncidentTemplateProps;

View File

@@ -0,0 +1,45 @@
import { Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SecretReminderTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
projectName: string;
organizationName: string;
reminderNote?: string;
}
export const SecretReminderTemplate = ({
siteUrl,
reminderNote,
projectName,
organizationName
}: SecretReminderTemplateProps) => {
return (
<BaseEmailWrapper title="Secret Reminder" preview="You have a new secret reminder." siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Secret Reminder</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">
You have a new secret reminder from the project <strong>{projectName}</strong> in the{" "}
<strong>{organizationName}</strong> organization.
</Text>
{reminderNote && (
<Text className="text-[14px] text-slate-700">
<strong className="text-black">Reminder Note:</strong> "{reminderNote}"
</Text>
)}
</Section>
</BaseEmailWrapper>
);
};
export default SecretReminderTemplate;
SecretReminderTemplate.PreviewProps = {
reminderNote: "Remember to rotate secret.",
projectName: "Example Project",
organizationName: "Example Organization",
siteUrl: "https://infisical.com"
} as SecretReminderTemplateProps;

View File

@@ -0,0 +1,53 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SecretRequestCompletedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
name?: string;
respondentUsername: string;
secretRequestUrl: string;
}
export const SecretRequestCompletedTemplate = ({
name,
siteUrl,
respondentUsername,
secretRequestUrl
}: SecretRequestCompletedTemplateProps) => {
return (
<BaseEmailWrapper title="Shared Secret" preview="A secret has been shared with you." siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>A secret has been shared with you</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] text-center pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">
{respondentUsername ? <strong>{respondentUsername}</strong> : "Someone"} shared a secret{" "}
{name && (
<>
<strong>{name}</strong>{" "}
</>
)}{" "}
with you.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={secretRequestUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
View Secret
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default SecretRequestCompletedTemplate;
SecretRequestCompletedTemplate.PreviewProps = {
respondentUsername: "Gracie",
siteUrl: "https://infisical.com",
secretRequestUrl: "https://infisical.com",
name: "API_TOKEN"
} as SecretRequestCompletedTemplateProps;

View File

@@ -0,0 +1,68 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SecretRotationFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
rotationType: string;
rotationName: string;
rotationUrl: string;
projectName: string;
environment: string;
secretPath: string;
content: string;
}
export const SecretRotationFailedTemplate = ({
rotationType,
rotationName,
rotationUrl,
projectName,
siteUrl,
environment,
secretPath,
content
}: SecretRotationFailedTemplateProps) => {
return (
<BaseEmailWrapper title="Secret Rotation Failed" preview="A secret rotation failed." siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
Your <strong>{rotationType}</strong> rotation <strong>{rotationName}</strong> failed to rotate
</Heading>
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<strong>Name</strong>
<Text className="text-[14px] mt-[4px]">{rotationName}</Text>
<strong>Type</strong>
<Text className="text-[14px] mt-[4px]">{rotationType}</Text>
<strong>Project</strong>
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
<strong>Environment</strong>
<Text className="text-[14px] mt-[4px]">{environment}</Text>
<strong>Secret Path</strong>
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
<strong>Reason:</strong>
<Text className="text-[14px] text-red-500 mt-[4px]">{content}</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={`${rotationUrl}?search=${rotationName}&secretPath=${secretPath}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
View in Infisical
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default SecretRotationFailedTemplate;
SecretRotationFailedTemplate.PreviewProps = {
rotationType: "Auth0 Client Secret",
rotationUrl: "https://infisical.com",
content: "See Rotation status for details",
projectName: "Example Project",
secretPath: "/api/secrets",
environment: "Production",
rotationName: "my-auth0-rotation",
siteUrl: "https://infisical.com"
} as SecretRotationFailedTemplateProps;

View File

@@ -0,0 +1,80 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SecretSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
syncDestination: string;
syncName: string;
syncUrl: string;
projectName: string;
environment: string;
secretPath: string;
failureMessage: string;
}
export const SecretSyncFailedTemplate = ({
syncDestination,
syncName,
syncUrl,
projectName,
siteUrl,
environment,
secretPath,
failureMessage
}: SecretSyncFailedTemplateProps) => {
return (
<BaseEmailWrapper title="Secret Sync Failed" preview="A secret sync failed." siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
Your <strong>{syncDestination}</strong> sync <strong>{syncName}</strong> failed to complete
</Heading>
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<strong>Name</strong>
<Text className="text-[14px] mt-[4px]">{syncName}</Text>
<strong>Destination</strong>
<Text className="text-[14px] mt-[4px]">{syncDestination}</Text>
<strong>Project</strong>
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
{environment && (
<>
<strong>Environment</strong>
<Text className="text-[14px] mt-[4px]">{environment}</Text>
</>
)}
{secretPath && (
<>
<strong>Secret Path</strong>
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
</>
)}
{failureMessage && (
<>
<strong>Reason:</strong>
<Text className="text-[14px] text-red-500 mt-[4px]">{failureMessage}</Text>
</>
)}
</Section>
<Section className="text-center mt-[28px]">
<Button
href={syncUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
View in Infisical
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default SecretSyncFailedTemplate;
SecretSyncFailedTemplate.PreviewProps = {
syncDestination: "AWS Parameter Store",
syncUrl: "https://infisical.com",
failureMessage: "Key name cannot contain a colon (:) or a forward slash (/).",
projectName: "Example Project",
secretPath: "/api/secrets",
environment: "Production",
syncName: "my-aws-sync",
siteUrl: "https://infisical.com"
} as SecretSyncFailedTemplateProps;

View File

@@ -0,0 +1,53 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ServiceTokenExpiryNoticeTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
tokenName: string;
projectName: string;
url: string;
}
export const ServiceTokenExpiryNoticeTemplate = ({
tokenName,
siteUrl,
projectName,
url
}: ServiceTokenExpiryNoticeTemplateProps) => {
return (
<BaseEmailWrapper
title="Service Token Expiring Soon"
preview="A service token is about to expire."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Service token expiry notice</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">
Your service token <strong>{tokenName}</strong> for the project <strong>{projectName}</strong> will expire
within 24 hours.
</Text>
<Text>If this token is still needed for your workflow, please create a new one before it expires.</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={url}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Create New Token
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default ServiceTokenExpiryNoticeTemplate;
ServiceTokenExpiryNoticeTemplate.PreviewProps = {
projectName: "Example Project",
siteUrl: "https://infisical.com",
url: "https://infisical.com",
tokenName: "Example Token"
} as ServiceTokenExpiryNoticeTemplateProps;

View File

@@ -0,0 +1,53 @@
import { Heading, Link, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SignupEmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
code: string;
isCloud: boolean;
}
export const SignupEmailVerificationTemplate = ({ code, siteUrl, isCloud }: SignupEmailVerificationTemplateProps) => {
return (
<BaseEmailWrapper
title="Confirm Your Email Address"
preview="Verify your email address to get started with Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Confirm your email address</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[8px] text-center pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text>Enter the confirmation code shown below in the browser where you started sign-up.</Text>
<Text className="text-[24px] mt-[16px]">
<strong>{code}</strong>
</Text>
</Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]">
<strong>Questions about setting up Infisical?</strong>{" "}
{isCloud ? (
<>
Email us at{" "}
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
support@infisical.com
</Link>
</>
) : (
"Contact your administrator"
)}
.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default SignupEmailVerificationTemplate;
SignupEmailVerificationTemplate.PreviewProps = {
code: "124356",
isCloud: true,
siteUrl: "https://infisical.com"
} as SignupEmailVerificationTemplateProps;

View File

@@ -0,0 +1,45 @@
import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface UnlockAccountTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
token: string;
callback_url: string;
}
export const UnlockAccountTemplate = ({ token, siteUrl, callback_url }: UnlockAccountTemplateProps) => {
return (
<BaseEmailWrapper
title="Your Infisical Account Has Been Locked"
preview="Unlock your Infisical account to continue."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Unlock your Infisical account</strong>
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">
Your account has been temporarily locked due to multiple failed login attempts.
</Text>
<Text>If these attempts were not made by you, reset your password immediately.</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={`${callback_url}?token=${token}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Unlock Account
</Button>
</Section>
</BaseEmailWrapper>
);
};
export default UnlockAccountTemplate;
UnlockAccountTemplate.PreviewProps = {
callback_url: "Example Project",
siteUrl: "https://infisical.com",
token: "preview-token"
} as UnlockAccountTemplateProps;

View File

@@ -0,0 +1,27 @@
export * from "./AccessApprovalRequestTemplate";
export * from "./EmailMfaTemplate";
export * from "./EmailVerificationTemplate";
export * from "./ExternalImportFailedTemplate";
export * from "./ExternalImportStartedTemplate";
export * from "./ExternalImportSucceededTemplate";
export * from "./IntegrationSyncFailedTemplate";
export * from "./NewDeviceLoginTemplate";
export * from "./OrgAdminBreakglassAccessTemplate";
export * from "./OrgAdminProjectGrantAccessTemplate";
export * from "./OrganizationInvitationTemplate";
export * from "./PasswordResetTemplate";
export * from "./PasswordSetupTemplate";
export * from "./PkiExpirationAlertTemplate";
export * from "./ProjectAccessRequestTemplate";
export * from "./ProjectInvitationTemplate";
export * from "./ScimUserProvisionedTemplate";
export * from "./SecretApprovalRequestBypassedTemplate";
export * from "./SecretApprovalRequestNeedsReviewTemplate";
export * from "./SecretLeakIncidentTemplate";
export * from "./SecretReminderTemplate";
export * from "./SecretRequestCompletedTemplate";
export * from "./SecretRotationFailedTemplate";
export * from "./SecretSyncFailedTemplate";
export * from "./ServiceTokenExpiryNoticeTemplate";
export * from "./SignupEmailVerificationTemplate";
export * from "./UnlockAccountTemplate";

View File

@@ -1,13 +1,41 @@
import fs from "node:fs/promises";
import path from "node:path";
import handlebars from "handlebars";
import { render } from "@react-email/components";
import { createTransport } from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";
import React from "react";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import {
AccessApprovalRequestTemplate,
EmailMfaTemplate,
EmailVerificationTemplate,
ExternalImportFailedTemplate,
ExternalImportStartedTemplate,
ExternalImportSucceededTemplate,
IntegrationSyncFailedTemplate,
NewDeviceLoginTemplate,
OrgAdminBreakglassAccessTemplate,
OrgAdminProjectGrantAccessTemplate,
OrganizationInvitationTemplate,
PasswordResetTemplate,
PasswordSetupTemplate,
PkiExpirationAlertTemplate,
ProjectAccessRequestTemplate,
ProjectInvitationTemplate,
ScimUserProvisionedTemplate,
SecretApprovalRequestBypassedTemplate,
SecretApprovalRequestNeedsReviewTemplate,
SecretLeakIncidentTemplate,
SecretReminderTemplate,
SecretRequestCompletedTemplate,
SecretRotationFailedTemplate,
SecretSyncFailedTemplate,
ServiceTokenExpiryNoticeTemplate,
SignupEmailVerificationTemplate,
UnlockAccountTemplate
} from "./emails";
export type TSmtpConfig = SMTPTransport.Options;
export type TSmtpSendMail = {
template: SmtpTemplates;
@@ -18,61 +46,96 @@ export type TSmtpSendMail = {
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
export enum SmtpTemplates {
SignupEmailVerification = "signupEmailVerification.handlebars",
EmailVerification = "emailVerification.handlebars",
SecretReminder = "secretReminder.handlebars",
EmailMfa = "emailMfa.handlebars",
UnlockAccount = "unlockAccount.handlebars",
AccessApprovalRequest = "accessApprovalRequest.handlebars",
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview.handlebars",
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",
ResetPassword = "passwordReset.handlebars",
SetupPassword = "passwordSetup.handlebars",
SecretLeakIncident = "secretLeakIncident.handlebars",
WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars",
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
IntegrationSyncFailed = "integrationSyncFailed.handlebars",
SecretSyncFailed = "secretSyncFailed.handlebars",
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
ExternalImportFailed = "externalImportFailed.handlebars",
ExternalImportStarted = "externalImportStarted.handlebars",
SecretRequestCompleted = "secretRequestCompleted.handlebars",
SecretRotationFailed = "secretRotationFailed.handlebars",
ProjectAccessRequest = "projectAccess.handlebars",
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars",
ServiceTokenExpired = "serviceTokenExpired.handlebars"
SignupEmailVerification = "signupEmailVerification",
EmailVerification = "emailVerification",
SecretReminder = "secretReminder",
EmailMfa = "emailMfa",
UnlockAccount = "unlockAccount",
AccessApprovalRequest = "accessApprovalRequest",
AccessSecretRequestBypassed = "accessSecretRequestBypassed",
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview",
// HistoricalSecretList = "historicalSecretLeakIncident", not used anymore?
NewDeviceJoin = "newDevice",
OrgInvite = "organizationInvitation",
ResetPassword = "passwordReset",
SetupPassword = "passwordSetup",
SecretLeakIncident = "secretLeakIncident",
WorkspaceInvite = "workspaceInvitation",
ScimUserProvisioned = "scimUserProvisioned",
PkiExpirationAlert = "pkiExpirationAlert",
IntegrationSyncFailed = "integrationSyncFailed",
SecretSyncFailed = "secretSyncFailed",
ExternalImportSuccessful = "externalImportSuccessful",
ExternalImportFailed = "externalImportFailed",
ExternalImportStarted = "externalImportStarted",
SecretRequestCompleted = "secretRequestCompleted",
SecretRotationFailed = "secretRotationFailed",
ProjectAccessRequest = "projectAccess",
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess",
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess",
ServiceTokenExpired = "serviceTokenExpired"
}
export enum SmtpHost {
Sendgrid = "smtp.sendgrid.net",
Mailgun = "smtp.mailgun.org",
SocketLabs = "smtp.sockerlabs.com",
SocketLabs = "smtp.socketlabs.com",
Zohomail = "smtp.zoho.com",
Gmail = "smtp.gmail.com",
Office365 = "smtp.office365.com"
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
[SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate,
[SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate,
[SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate,
[SmtpTemplates.EmailMfa]: EmailMfaTemplate,
[SmtpTemplates.AccessApprovalRequest]: AccessApprovalRequestTemplate,
[SmtpTemplates.EmailVerification]: EmailVerificationTemplate,
[SmtpTemplates.ExternalImportFailed]: ExternalImportFailedTemplate,
[SmtpTemplates.ExternalImportStarted]: ExternalImportStartedTemplate,
[SmtpTemplates.ExternalImportSuccessful]: ExternalImportSucceededTemplate,
[SmtpTemplates.AccessSecretRequestBypassed]: SecretApprovalRequestBypassedTemplate,
[SmtpTemplates.IntegrationSyncFailed]: IntegrationSyncFailedTemplate,
[SmtpTemplates.OrgAdminBreakglassAccess]: OrgAdminBreakglassAccessTemplate,
[SmtpTemplates.SecretLeakIncident]: SecretLeakIncidentTemplate,
[SmtpTemplates.WorkspaceInvite]: ProjectInvitationTemplate,
[SmtpTemplates.ScimUserProvisioned]: ScimUserProvisionedTemplate,
[SmtpTemplates.SecretRequestCompleted]: SecretRequestCompletedTemplate,
[SmtpTemplates.UnlockAccount]: UnlockAccountTemplate,
[SmtpTemplates.ServiceTokenExpired]: ServiceTokenExpiryNoticeTemplate,
[SmtpTemplates.SecretReminder]: SecretReminderTemplate,
[SmtpTemplates.SecretRotationFailed]: SecretRotationFailedTemplate,
[SmtpTemplates.SecretSyncFailed]: SecretSyncFailedTemplate,
[SmtpTemplates.OrgAdminProjectDirectAccess]: OrgAdminProjectGrantAccessTemplate,
[SmtpTemplates.ProjectAccessRequest]: ProjectAccessRequestTemplate,
[SmtpTemplates.SecretApprovalRequestNeedsReview]: SecretApprovalRequestNeedsReviewTemplate,
[SmtpTemplates.ResetPassword]: PasswordResetTemplate,
[SmtpTemplates.SetupPassword]: PasswordSetupTemplate,
[SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate
};
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
const smtp = createTransport(cfg);
const isSmtpOn = Boolean(cfg.host);
handlebars.registerHelper("emailFooter", () => {
const { SITE_URL } = getConfig();
return new handlebars.SafeString(
`<p style="font-size: 12px;">Email sent via Infisical at <a href="${SITE_URL}">${SITE_URL}</a></p>`
);
});
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
const appCfg = getConfig();
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
const temp = handlebars.compile(html);
const htmlToSend = temp({ isCloud: appCfg.isCloud, siteUrl: appCfg.SITE_URL, ...substitutions });
const EmailTemplate = EmailTemplateMap[template];
if (!EmailTemplate) {
throw new Error(`Email template ${template} not found`);
}
const htmlToSend = await render(
React.createElement(EmailTemplate, {
...substitutions,
isCloud: appCfg.isCloud,
siteUrl: appCfg.SITE_URL
})
);
if (isSmtpOn) {
await smtp.sendMail({

View File

@@ -1,55 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Access Approval Request</title>
</head>
<body>
<h2>Infisical</h2>
<h2>New access approval request pending your review</h2>
<p>You have a new access approval request pending review in project "{{projectName}}".</p>
<p>
{{requesterFullName}}
({{requesterEmail}}) has requested
{{#if isTemporary}}
temporary
{{else}}
permanent
{{/if}}
access to
{{secretPath}}
in the
{{environment}}
environment.
{{#if isTemporary}}
<br />
This access will expire
{{expiresIn}}
after it has been approved.
{{/if}}
</p>
<p>
The following permissions are requested:
<ul>
{{#each permissions}}
<li>{{this}}</li>
{{/each}}
</ul>
</p>
{{#if note}}
<p>User Note: "{{note}}"</p>
{{/if}}
<p>
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,33 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Approval Request Policy Bypassed</title>
</head>
<body>
<h1>Infisical</h1>
<h2>Secret Approval Request Bypassed</h2>
<p>A secret approval request has been bypassed in the project "{{projectName}}".</p>
<p>
{{requesterFullName}}
({{requesterEmail}}) has merged a secret to environment
{{environment}}
at secret path
{{secretPath}}
without obtaining the required approvals.
</p>
<p>
The following reason was provided for bypassing the policy:
<em>{{bypassReason}}</em>
</p>
<p>
To review this action, please visit the request panel
<a href="{{approvalUrl}}">here</a>.
</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,20 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>MFA Code</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Sign in attempt requires further verification</h2>
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
<h2>{{code}}</h2>
<p>The MFA code will be valid for 2 minutes.</p>
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,17 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Code</title>
</head>
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
<h1>{{code}}</h1>
{{emailFooter}}
</body>
</html>

View File

@@ -1,22 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import failed</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical has failed</h2>
<p>An import from
{{provider}}
to Infisical has failed due to unforeseen circumstances. Please re-try your import, and if the issue persists, you
can contact the Infisical team at team@infisical.com.
</p>
<p>Error: {{error}}</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,19 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import in progress</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical is in progress</h2>
<p>An import from
{{provider}}
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
has finished or if it fails.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import successful</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical was successful</h2>
<p>An import from {{provider}} was successful. Your data is now available in Infisical.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,21 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Incident alert: secrets potentially leaked</title>
</head>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,33 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Integration Sync Failed</title>
</head>
<body>
<h2>Infisical</h2>
<div>
<p>{{count}} integration(s) failed to sync.</p>
<a href="{{integrationUrl}}">
View your project integrations.
</a>
</div>
<br />
<div>
<p><strong>Project</strong>: {{projectName}}</p>
<p><strong>Environment</strong>: {{environment}}</p>
<p><strong>Secret Path</strong>: {{secretPath}}</p>
</div>
{{#if syncMessage}}
<p><b>Reason: </b>{{syncMessage}}</p>
{{/if}}
{{emailFooter}}
</body>
</html>

View File

@@ -1,22 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Successful login for {{email}} from new device</title>
</head>
<body>
<h2>Infisical</h2>
<p>We're verifying a recent login for {{email}}:</p>
<p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</p>
<p>If you believe that this login is suspicious, please contact
{{#if isCloud}}Infisical{{else}}your administrator{{/if}}
or reset your password immediately.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,20 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Organization admin has bypassed SSO</title>
</head>
<body>
<h2>Infisical</h2>
<p>The organization admin {{email}} has bypassed enforced SSO login.</p>
<p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</p>
<p>If you'd like to disable Admin SSO Bypass, please visit <a href="{{siteUrl}}/organization/settings">Organization Settings</a> > Security.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Organization admin issued direct access to project</title>
</head>
<body>
<h2>Infisical</h2>
<p>The organization admin {{email}} has granted direct access to the project "{{projectName}}".</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Organization Invitation</title>
</head>
<body>
<h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization named {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Click to join</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Account Recovery</title>
</head>
<body>
<h2>Reset your password</h2>
<p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
<p>If you didn't initiate this request, please contact
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,17 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Password Setup</title>
</head>
<body>
<h2>Setup your password</h2>
<p>Someone requested to set up a password for your account.</p>
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
<p>If you didn't initiate this request, please contact
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,33 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Infisical CA/Certificate expiration notice</title>
</head>
<body>
<p>Hello,</p>
<p>This is an automated alert for "{{alertName}}" triggered for CAs/Certificates expiring in
{{alertBeforeDays}}
days.</p>
<p>Expiring Items:</p>
<ul>
{{#each items}}
<li>
{{type}}:
<strong>{{friendlyName}}</strong>
<br />Serial Number:
{{serialNumber}}
<br />Expires On:
{{expiryDate}}
</li>
{{/each}}
</ul>
<p>Please take necessary actions to renew these items before they expire.</p>
<p>For more details, please log in to your Infisical account and check your PKI management section.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,26 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Project Access Request</title>
</head>
<body>
<h2>Infisical</h2>
<h2>You have a new project access request!</h2>
<ul>
<li>Requester Name: "{{requesterName}}"</li>
<li>Requester Email: "{{requesterEmail}}"</li>
<li>Project Name: "{{projectName}}"</li>
<li>Organization Name: "{{orgName}}"</li>
<li>User Note: "{{note}}"</li>
</ul>
<p>
Please click on the link below to grant access
</p>
<a href="{{callback_url}}">Grant Access</a>
{{emailFooter}}
</body>
</html>

View File

@@ -1,18 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Organization Invitation</title>
</head>
<body>
<h2>Join your organization on Infisical</h2>
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,24 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Change Approval Request</title>
</head>
<body>
<h2>Hi {{firstName}},</h2>
<h2>New secret change requests are pending review.</h2>
<br />
<p>You have a secret change request pending your review in project "{{projectName}}", in the "{{organizationName}}"
organization.</p>
<p>
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,27 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Incident alert: secret leaked</title>
</head>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by
{{pusher_name}}
({{pusher_email}}). If these are test secrets, please add `infisical-scan:ignore` at the end of the line
containing the secret as comment in the given programming. This will prevent future notifications from being sent
out for those secret(s).</p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,20 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Reminder</title>
</head>
<body>
<h2>Infisical</h2>
<h2>You have a new secret reminder!</h2>
<p>You have a new secret reminder from project "{{projectName}}", in {{organizationName}}</p>
{{#if reminderNote}}
<p>Here's the note included with the reminder: {{reminderNote}}</p>
{{/if}}
{{emailFooter}}
</body>
</html>

View File

@@ -1,33 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Request Completed</title>
</head>
<body>
<h2>Infisical</h2>
<h2>A secret has been shared with you</h2>
{{#if name}}
<p>Secret request name: {{name}}</p>
{{/if}}
{{#if respondentUsername}}
<p>Shared by: {{respondentUsername}}</p>
{{/if}}
<br />
<br/>
<p>
You can access the secret by clicking the link below.
</p>
<p>
<a href="{{secretRequestUrl}}">Access Secret</a>
</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,31 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Your {{rotationType}} Rotation "{{rotationName}}" Failed to Rotate</title>
</head>
<body>
<h2>Infisical</h2>
<div>
<p>{{content}}</p>
<a href="{{rotationUrl}}?search={{rotationName}}&secretPath={{secretPath}}">
View in Infisical to see the cause of failure.
</a>
</div>
<br />
<div>
<p><strong>Name</strong>: {{rotationName}}</p>
<p><strong>Type</strong>: {{rotationType}}</p>
<p><strong>Project</strong>: {{projectName}}</p>
<p><strong>Environment</strong>: {{environment}}</p>
<p><strong>Secret Path</strong>: {{secretPath}}</p>
</div>
{{emailFooter}}
</body>
</html>

View File

@@ -1,39 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>{{syncDestination}} Sync "{{syncName}}" Failed</title>
</head>
<body>
<h2>Infisical</h2>
<div>
<p>{{content}}</p>
<a href="{{syncUrl}}">
View in Infisical.
</a>
</div>
<br />
<div>
<p><strong>Name</strong>: {{syncName}}</p>
<p><strong>Destination</strong>: {{syncDestination}}</p>
<p><strong>Project</strong>: {{projectName}}</p>
{{#if environment}}
<p><strong>Environment</strong>: {{environment}}</p>
{{/if}}
{{#if secretPath}}
<p><strong>Secret Path</strong>: {{secretPath}}</p>
{{/if}}
</div>
{{#if failureMessage}}
<p><b>Reason: </b>{{failureMessage}}</p>
{{/if}}
{{emailFooter}}
</body>
</html>

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Service Token Expiring Soon</title>
</head>
<body>
<h2>Service Token Expiry Notice</h2>
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
<p>This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.</p>
<a href="{{url}}">Create New Token</a>
{{emailFooter}}
</body>
</html>

View File

@@ -1,19 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Code</title>
</head>
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical?
{{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,18 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Your Infisical account has been locked</title>
</head>
<body>
<h2>Unlock your Infisical account</h2>
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
<a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a>
<p>If these attempts were not made by you, reset your password immediately.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -1,17 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Project Invitation</title>
</head>
<body>
<h2>Join your team on Infisical</h2>
<p>You have been invited to a new Infisical project named {{workspaceName}}</p>
<a href="{{callback_url}}">Click to join</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -176,8 +176,8 @@ export const superAdminServiceFactory = ({
const canServerAdminAccessAfterApply =
data.enabledLoginMethods.some((loginMethod) =>
loginMethodToAuthMethod[loginMethod as LoginMethod].some(
(authMethod) => superAdminUser.authMethods?.includes(authMethod)
loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) =>
superAdminUser.authMethods?.includes(authMethod)
)
) ||
isUserSamlAccessEnabled ||

View File

@@ -25,7 +25,8 @@
"baseUrl": ".",
"paths": {
"@app/*": ["./src/*"]
}
},
"jsx": "react-jsx"
},
"include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"],
"exclude": ["node_modules"]

View File

@@ -2,8 +2,8 @@
import path from "node:path";
import fs from "fs/promises";
import { replaceTscAliasPaths } from "tsc-alias";
import { defineConfig } from "tsup";
import {replaceTscAliasPaths} from "tsc-alias";
import {defineConfig} from "tsup";
// Instead of using tsx or tsc for building, consider using tsup.
// TSX serves as an alternative to Node.js, allowing you to build directly on the Node.js runtime.
@@ -50,7 +50,17 @@ export default defineConfig({
const isFile = await fs
.stat(`${absPath}.ts`)
.then((el) => el.isFile)
.catch((err) => err.code === "ENOTDIR");
.catch(async (err) => {
if (err.code === "ENOTDIR") {
return true;
}
// If .ts file doesn't exist, try checking for .tsx file
return fs
.stat(`${absPath}.tsx`)
.then((el) => el.isFile)
.catch((err) => err.code === "ENOTDIR");
});
return {
path: isFile ? `${args.path}.mjs` : `${args.path}/index.mjs`,

View File

@@ -73,6 +73,61 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
</Warning>
</Tab>
<Tab title="Production (Helm)">
The Gateway can be installed via [Helm](https://helm.sh/). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications.
For production deployments on Kubernetes, install the Gateway using the Infisical Helm chart:
### Install the latest Helm Chart repository
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
```
### Update the Helm Chart repository
```bash
helm repo update
```
### Create a Kubernetes Secret with the gateway token
Create a new Kubernetes secret containing the gateway token as the `TOKEN` key. You can optionally also set the `INFISICAL_API_URL` key to your Infisical instance URL. By default, `INFISICAL_API_URL` is set to `https://app.infisical.com`.
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=<your-machine-identity-access-token>
```
<Note>
The secret name is `infisical-gateway-environment` by default. The `TOKEN` key is required, and the `INFISICAL_API_URL` key is optional.
</Note>
### Install the Infisical Gateway Helm Chart
```bash
helm install infisical-gateway infisical-helm-charts/infisical-gateway
```
### Check the gateway logs
After installing the gateway, you can check the logs to ensure it's running as expected.
```bash
kubectl logs deployment/infisical-gateway
```
You should see the following output which indicates the gateway is running as expected.
```bash
$ kubectl logs deployment/infisical-gateway
INF Provided relay port 5349. Using TLS
INF Connected with relay
INF 10.0.101.112:56735
INF Starting relay connection health check
INF Gateway started successfully
INF New connection from: 10.0.1.8:34051
INF Gateway is reachable by Infisical
```
</Tab>
<Tab title="Development (direct)">
For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command:
```bash

View File

@@ -1230,13 +1230,13 @@ To address this, we added functionality to automatically redeploy your deploymen
#### Enabling Automatic Redeployment
To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret.
To enable auto redeployment you simply have to add the following annotation to the Deployment, StatefulSet, or DaemonSet that consumes a managed secret.
```yaml
secrets.infisical.com/auto-reload: "true"
```
<Accordion title="Deployment example with auto redeploy enabled">
<Accordion title="Deployment example">
```yaml
apiVersion: apps/v1
kind: Deployment
@@ -1266,10 +1266,82 @@ secrets.infisical.com/auto-reload: "true"
- containerPort: 80
```
</Accordion>
<Accordion title="DaemonSet example">
```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-agent
labels:
app: log-agent
annotations:
secrets.infisical.com/auto-reload: "true" # <- redeployment annotation
spec:
selector:
matchLabels:
app: log-agent
template:
metadata:
labels:
app: log-agent
spec:
containers:
- name: log-agent
image: mycompany/log-agent:latest
envFrom:
- secretRef:
name: managed-secret # <- name of the managed secret
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
volumes:
- name: config-volume
secret:
secretName: managed-secret
```
</Accordion>
<Accordion title="StatefulSet example">
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db-worker
labels:
app: db-worker
annotations:
secrets.infisical.com/auto-reload: "true" # <- redeployment annotation
spec:
selector:
matchLabels:
app: db-worker
serviceName: "db-worker"
replicas: 2
template:
metadata:
labels:
app: db-worker
spec:
containers:
- name: db-worker
image: mycompany/db-worker:stable
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: managed-secret
key: DB_PASSWORD
ports:
- containerPort: 5432
```
</Accordion>
<Info>
#### How it works
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
Then, for each deployment that has this annotation present, a rolling update will be triggered.
When a managed secret is updated, the operator checks for any Deployments, DaemonSets, or StatefulSets that consume the updated secret and have the annotation
`secrets.infisical.com/auto-reload: "true"`. For each matching workload, the operator triggers a rolling restart to ensure it picks up the latest secret values.
</Info>
## Using Managed ConfigMap In Your Deployment

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,3 @@
## 0.0.1 (May 1, 2025)
* Initial helm release

View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: infisical-gateway
description: A Helm chart to deploy Infisical Gateway
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.0.1"

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "infisical-gateway.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "infisical-gateway.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "infisical-gateway.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "infisical-gateway.labels" -}}
helm.sh/chart: {{ include "infisical-gateway.chart" . }}
{{ include "infisical-gateway.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "infisical-gateway.selectorLabels" -}}
app.kubernetes.io/name: {{ include "infisical-gateway.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "infisical-gateway.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "infisical-gateway.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,73 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "infisical-gateway.fullname" . }}
labels:
{{- include "infisical-gateway.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "infisical-gateway.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "infisical-gateway.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "infisical-gateway.serviceAccountName" . }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
image: "infisical/cli:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- gateway
- --token
- $(TOKEN)
envFrom:
- secretRef:
name: {{ .Values.secret.name }}
env:
- name: TOKEN_VALIDATION
valueFrom:
secretKeyRef:
name: {{ .Values.secret.name }}
key: TOKEN
optional: false
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "infisical-gateway.fullname" . }}
labels:
{{- include "infisical-gateway.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "infisical-gateway.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "infisical-gateway.serviceAccountName" . }}
labels:
{{- include "infisical-gateway.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}

View File

@@ -0,0 +1,44 @@
image:
pullPolicy: IfNotPresent
tag: "0.41.1"
secret:
# The secret that contains the environment variables to be used by the gateway, such as INFISICAL_API_URL and TOKEN
name: "infisical-gateway-environment"
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
automount: true
annotations: {}
name: ""
podAnnotations: {}
podLabels: {}
podSecurityContext:
runAsNonRoot: true
securityContext:
runAsNonRoot: true
runAsUser: 1000
affinity: {}
tolerations: {}
nodeSelector: {}
service:
type: ClusterIP
port: 80
ingress:
enabled: false

View File

@@ -0,0 +1,8 @@
cd infisical-gateway
helm dependency update
helm package .
for i in *.tgz; do
[ -f "$i" ] || break
cloudsmith push helm --republish infisical/helm-charts "$i"
done
cd ..