Compare commits

...

1 Commits

69 changed files with 4278 additions and 873 deletions

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"
}
]
}
};

2546
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",
@ -222,6 +226,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

@ -397,8 +397,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)) {
@ -569,9 +569,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

@ -282,7 +282,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

@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { generateCacheKeyFromData } from "@app/lib/crypto/cache";
import { applyJitter } from "@app/lib/dates";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import {
buildFindFilter,
@ -22,7 +23,6 @@ import type {
TFindSecretsByFolderIdsFilter,
TGetSecretsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { applyJitter } from "@app/lib/dates";
export const SecretServiceCacheKeys = {
get productKey() {

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 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 get 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 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,30 @@
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"
} 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 has 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@infiscal.com"
} as OrgAdminBreakglassAccessTemplateProps;

View File

@ -0,0 +1,40 @@
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"
} as OrgAdminProjectGrantAccessTemplateProps;

View File

@ -0,0 +1,75 @@
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=${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"
} as OrganizationInvitationTemplateProps;

View File

@ -0,0 +1,58 @@
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=${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
} as PasswordResetTemplateProps;

View File

@ -0,0 +1,57 @@
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=${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
} as PasswordSetupTemplateProps;

View File

@ -0,0 +1,68 @@
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: "2022-01-01" },
{ type: "Certificate", friendlyName: "Example Certificate", serialNumber: "1234567890", expiryDate: "2022-01-01" }
],
alertName: "My PKI Alert"
} 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;
environment: 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."
} 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 new 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 you 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}/secret-scanning`} className="text-slate-700 no-underline">
Infisical Dashboard
</Link>
.
</Text>
</Section>
<Section className="text-center mt-[28px]">
<Button
href={`${siteUrl}/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">
Secret Reminder
</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 ShareSecretTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
name?: string;
respondentUsername: string;
secretRequestUrl: string;
}
export const SecretRequestCompletedTemplate = ({
name,
siteUrl,
respondentUsername,
secretRequestUrl
}: ShareSecretTemplateProps) => {
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 ShareSecretTemplateProps;

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 Infiscal
</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 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>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 Infiscal
</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 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,46 @@
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",
url: "https://infisical.com",
token: "Example Token"
} as UnlockAccountTemplateProps;

View File

@ -0,0 +1,28 @@
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 "./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,112 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
import handlebars from "handlebars";
import { createTransport } from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
export type TSmtpConfig = SMTPTransport.Options;
export type TSmtpSendMail = {
template: SmtpTemplates;
subjectLine: string;
recipients: string[];
substitutions: object;
};
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"
}
export enum SmtpHost {
Sendgrid = "smtp.sendgrid.net",
Mailgun = "smtp.mailgun.org",
SocketLabs = "smtp.sockerlabs.com",
Zohomail = "smtp.zoho.com",
Gmail = "smtp.gmail.com",
Office365 = "smtp.office365.com"
}
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 });
if (isSmtpOn) {
await smtp.sendMail({
from: cfg.from,
to: recipients.join(", "),
subject: subjectLine,
html: htmlToSend
});
} else {
logger.info("SMTP is not configured. Outputting it in terminal");
logger.info({
from: cfg.from,
to: recipients.join(", "),
subject: subjectLine,
html: htmlToSend
});
}
};
const verify = async () => {
const isConnected = smtp
.verify()
.then(async () => {
logger.info("SMTP connected");
return true;
})
.catch((err: Error) => {
logger.error("SMTP error");
logger.error(err);
return false;
});
return isConnected;
};
return { sendMail, verify };
};

View File

@ -0,0 +1,179 @@
import { render } from "@react-email/components";
import handlebars from "handlebars";
import { createTransport } from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";
import React from "react";
import {
AccessApprovalRequestTemplate,
EmailMfaTemplate,
EmailVerificationTemplate,
ExternalImportFailedTemplate,
ExternalImportStartedTemplate,
ExternalImportSucceededTemplate,
IntegrationSyncFailedTemplate,
NewDeviceLoginTemplate,
OrgAdminBreakglassAccessTemplate,
OrganizationInvitationTemplate,
PasswordResetTemplate,
PasswordSetupTemplate,
PkiExpirationAlertTemplate,
ProjectAccessRequestTemplate,
ProjectInvitationTemplate,
ScimUserProvisionedTemplate,
SecretApprovalRequestBypassedTemplate,
SecretApprovalRequestNeedsReviewTemplate,
SecretLeakIncidentTemplate,
SecretReminderTemplate,
SecretRequestCompletedTemplate,
SecretRotationFailedTemplate,
SecretSyncFailedTemplate,
ServiceTokenExpiryNoticeTemplate,
SignupEmailVerificationTemplate,
UnlockAccountTemplate
} from "src/services/smtp/emails";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import OrgAdminProjectGrantAccessTemplate from "./emails/OrgAdminProjectGrantAccessTemplate";
export type TSmtpConfig = SMTPTransport.Options;
export type TSmtpSendMail = {
template: SmtpTemplates;
subjectLine: string;
recipients: string[];
substitutions: object;
};
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
export enum SmtpTemplates {
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",
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 EmailTemplate = EmailTemplateMap[template];
if (!EmailTemplate) {
throw new Error(`Email template ${template} not found`);
}
const htmlToSend = await render(
<EmailTemplate {...substitutions} isCloud={appCfg.isCloud} siteUrl={appCfg.SITE_URL} />
);
if (isSmtpOn) {
await smtp.sendMail({
from: cfg.from,
to: recipients.join(", "),
subject: subjectLine,
html: htmlToSend
});
} else {
logger.info("SMTP is not configured. Outputting it in terminal");
logger.info({
from: cfg.from,
to: recipients.join(", "),
subject: subjectLine,
html: htmlToSend
});
}
};
const verify = async () => {
const isConnected = smtp
.verify()
.then(async () => {
logger.info("SMTP connected");
return true;
})
.catch((err: Error) => {
logger.error("SMTP error");
logger.error(err);
return false;
});
return isConnected;
};
return { sendMail, verify };
};

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,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

@ -163,8 +163,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"
},
"include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"],
"exclude": ["node_modules"]