From e81c49500bcaa905ce61acb399dcdc8c10d20fc1 Mon Sep 17 00:00:00 2001 From: x Date: Tue, 29 Apr 2025 20:34:39 -0400 Subject: [PATCH 01/33] get certificate private key endpoint + migrations --- ...9232917_store-cert-secret-key-and-chain.ts | 33 +++++++++++++ backend/src/db/schemas/certificate-bodies.ts | 3 +- backend/src/db/schemas/certificate-secrets.ts | 5 +- backend/src/db/schemas/organizations.ts | 1 - backend/src/db/schemas/projects.ts | 2 +- .../server/routes/v1/certificate-router.ts | 45 +++++++++++++++++ .../services/certificate/certificate-fns.ts | 48 ++++++++++++++++++- .../certificate/certificate-secret-dal.ts | 10 ++++ .../certificate/certificate-service.ts | 40 +++++++++++++++- .../services/certificate/certificate-types.ts | 12 +++++ 10 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts create mode 100644 backend/src/services/certificate/certificate-secret-dal.ts diff --git a/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts new file mode 100644 index 000000000..2062f9a38 --- /dev/null +++ b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts @@ -0,0 +1,33 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateBody)) { + await knex.schema.alterTable(TableName.CertificateBody, (t) => { + t.binary("encryptedCertificateChain").nullable(); + }); + } + + if (!(await knex.schema.hasTable(TableName.CertificateSecret))) { + await knex.schema.createTable(TableName.CertificateSecret, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("certId").notNullable().unique(); + t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE"); + t.binary("encryptedPrivateKey").notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Certificate)) { + await knex.schema.dropTable(TableName.CertificateSecret); + } + + if (await knex.schema.hasTable(TableName.CertificateBody)) { + await knex.schema.alterTable(TableName.CertificateBody, (t) => { + t.dropColumn("encryptedCertificateChain"); + }); + } +} diff --git a/backend/src/db/schemas/certificate-bodies.ts b/backend/src/db/schemas/certificate-bodies.ts index 75afbddbd..10171e383 100644 --- a/backend/src/db/schemas/certificate-bodies.ts +++ b/backend/src/db/schemas/certificate-bodies.ts @@ -14,7 +14,8 @@ export const CertificateBodiesSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), certId: z.string().uuid(), - encryptedCertificate: zodBuffer + encryptedCertificate: zodBuffer, + encryptedCertificateChain: zodBuffer.nullable().optional() }); export type TCertificateBodies = z.infer; diff --git a/backend/src/db/schemas/certificate-secrets.ts b/backend/src/db/schemas/certificate-secrets.ts index f8cad74f1..75e6377b2 100644 --- a/backend/src/db/schemas/certificate-secrets.ts +++ b/backend/src/db/schemas/certificate-secrets.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const CertificateSecretsSchema = z.object({ @@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), certId: z.string().uuid(), - pk: z.string(), - sk: z.string() + encryptedPrivateKey: zodBuffer }); export type TCertificateSecrets = z.infer; diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 902c564a7..eea1808e0 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -23,7 +23,6 @@ export const OrganizationsSchema = z.object({ defaultMembershipRole: z.string().default("member"), enforceMfa: z.boolean().default(false), selectedMfaMethod: z.string().nullable().optional(), - secretShareSendToAnyone: z.boolean().default(true).nullable().optional(), allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(), shouldUseNewPrivilegeSystem: z.boolean().default(true), privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 2403d6cf4..297601fd0 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({ description: z.string().nullable().optional(), type: z.string(), enforceCapitalization: z.boolean().default(false), - hasDeleteProtection: z.boolean().default(true).nullable().optional() + hasDeleteProtection: z.boolean().default(false).nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index ea33e948f..42c515795 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -64,6 +64,51 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + // TODO(andrey): In the future add support for other formats outside of PEM. Adding a "format" query param may be best. + server.route({ + method: "GET", + url: "/:serialNumber/private-key", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate private key", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + }), + response: { + 200: z.string().trim() + } + }, + handler: async (req) => { + const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CERT, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + return certPrivateKey; + } + }); + server.route({ method: "POST", url: "/issue-certificate", diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 45ad5963c..55b731bd7 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -1,6 +1,11 @@ +import crypto from "node:crypto"; + import * as x509 from "@peculiar/x509"; -import { CrlReason } from "./certificate-types"; +import { NotFoundError } from "@app/lib/errors"; + +import { getProjectKmsCertificateKeyId } from "../project/project-fns"; +import { CrlReason, TGetCertificateCredentialsDTO } from "./certificate-types"; export const revocationReasonToCrlCode = (crlReason: CrlReason) => { switch (crlReason) { @@ -46,3 +51,44 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) .map((cert) => cert.toString("pem")) .join("\n") .trim(); + +/** + * Return the public and private key of certificate + * Note: credentials are returned as PEM strings + */ +export const getCertificateCredentials = async ({ + certId, + projectId, + certificateSecretDAL, + projectDAL, + kmsService +}: TGetCertificateCredentialsDTO) => { + const certificateSecret = await certificateSecretDAL.findOne({ certId }); + if (!certificateSecret) + throw new NotFoundError({ message: `Certificate secret for certificate with ID '${certId}' not found` }); + + const keyId = await getProjectKmsCertificateKeyId({ + projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + const decryptedPrivateKey = await kmsDecryptor({ + cipherTextBlob: certificateSecret.encryptedPrivateKey + }); + + const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" }); + const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString(); + + const pkObj = crypto.createPublicKey(skObj); + const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString(); + + return { + certificateSecret, + certPrivateKey, + certPublicKey + }; +}; diff --git a/backend/src/services/certificate/certificate-secret-dal.ts b/backend/src/services/certificate/certificate-secret-dal.ts new file mode 100644 index 000000000..d7f3e43ae --- /dev/null +++ b/backend/src/services/certificate/certificate-secret-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TCertificateSecretDALFactory = ReturnType; + +export const certificateSecretDALFactory = (db: TDbClient) => { + const caSecretOrm = ormify(db, TableName.CertificateSecret); + return caSecretOrm; +}; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 0ca0d64c6..d72235781 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -15,11 +15,13 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; -import { revocationReasonToCrlCode } from "./certificate-fns"; +import { getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; +import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; type TCertificateServiceFactoryDep = { certificateDAL: Pick; + certificateSecretDAL: Pick; certificateBodyDAL: Pick; certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; @@ -34,6 +36,7 @@ export type TCertificateServiceFactory = ReturnType { + const cert = await certificateDAL.findOne({ serialNumber }); + const ca = await certificateAuthorityDAL.findById(cert.caId); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + // TODO(andrey): Update permission for privateKey fetching. Should be very strict. + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + + const { certPrivateKey } = await getCertificateCredentials({ + certId: ca.id, + projectId: ca.projectId, + certificateSecretDAL, + projectDAL, + kmsService + }); + + return { + ca, + cert, + certPrivateKey + }; + }; + /** * Delete certificate with serial number [serialNumber] */ @@ -203,6 +240,7 @@ export const certificateServiceFactory = ({ return { getCert, + getCertPrivateKey, deleteCert, revokeCert, getCertBody diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index ef63f142d..48454f803 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509"; import { TProjectPermission } from "@app/lib/types"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { TProjectDALFactory } from "../project/project-dal"; +import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; + export enum CertStatus { ACTIVE = "active", REVOKED = "revoked" @@ -73,3 +77,11 @@ export type TRevokeCertDTO = { export type TGetCertBodyDTO = { serialNumber: string; } & Omit; + +export type TGetCertificateCredentialsDTO = { + certId: string; + projectId: string; + certificateSecretDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; From e47577491051c9ae75369d134932980867be1403 Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 00:33:46 -0400 Subject: [PATCH 02/33] made certificates store PK and chain in relation to the main table, added /bundle endpoints, new audit log and permission entries --- .../ee/services/audit-log/audit-log-types.ts | 22 ++++ .../services/permission/project-permission.ts | 9 ++ backend/src/lib/api-docs/constants.ts | 3 +- backend/src/server/routes/index.ts | 4 + .../server/routes/v1/certificate-router.ts | 62 +++++++++- .../certificate-authority-service.ts | 41 +++++-- .../services/certificate/certificate-fns.ts | 3 +- .../certificate/certificate-secret-dal.ts | 4 +- .../certificate/certificate-service.ts | 111 +++++++++++++++++- .../services/certificate/certificate-types.ts | 4 + 10 files changed, 239 insertions(+), 24 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b6527f3f4..93d3f03c3 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -215,6 +215,8 @@ export enum EventType { DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", GET_CERT_BODY = "get-cert-body", + GET_CERT_PRIVATE_KEY = "get-cert-private-key", + GET_CERT_BUNDLE = "get-cert-bundle", CREATE_PKI_ALERT = "create-pki-alert", GET_PKI_ALERT = "get-pki-alert", UPDATE_PKI_ALERT = "update-pki-alert", @@ -1719,6 +1721,24 @@ interface GetCertBody { }; } +interface GetCertPrivateKey { + type: EventType.GET_CERT_PRIVATE_KEY; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + +interface GetCertBundle { + type: EventType.GET_CERT_BUNDLE; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + interface CreatePkiAlert { type: EventType.CREATE_PKI_ALERT; metadata: { @@ -2691,6 +2711,8 @@ export type Event = | DeleteCert | RevokeCert | GetCertBody + | GetCertPrivateKey + | GetCertBundle | CreatePkiAlert | GetPkiAlert | UpdatePkiAlert diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 8e6645073..7d659e99b 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -129,6 +129,7 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", + CertificatePrivateKey = "certificate-private-key", CertificateTemplates = "certificate-templates", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificates = "ssh-certificates", @@ -232,6 +233,7 @@ export type ProjectPermissionSet = ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] + | [ProjectPermissionActions, ProjectPermissionSub.CertificatePrivateKey] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] @@ -480,6 +482,12 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), + z.object({ + subject: z.literal(ProjectPermissionSub.CertificatePrivateKey).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ) + }), z.object({ subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -681,6 +689,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.IpAllowList, ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.Certificates, + ProjectPermissionSub.CertificatePrivateKey, ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 19ee7e331..c8fb3820c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1580,7 +1580,8 @@ export const CERTIFICATES = { serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.", certificate: "The certificate body of the certificate.", certificateChain: "The certificate chain of the certificate.", - serialNumberRes: "The serial number of the certificate." + serialNumberRes: "The serial number of the certificate.", + privateKey: "The private key of the certificate." } }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 8ceeba648..f9a2223ee 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -260,6 +260,7 @@ import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; +import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -791,6 +792,7 @@ export const registerRoutes = async ( const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); + const certificateSecretDAL = certificateSecretDALFactory(db); const pkiAlertDAL = pkiAlertDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db); @@ -799,6 +801,7 @@ export const registerRoutes = async ( const certificateService = certificateServiceFactory({ certificateDAL, certificateBodyDAL, + certificateSecretDAL, certificateAuthorityDAL, certificateAuthorityCertDAL, certificateAuthorityCrlDAL, @@ -858,6 +861,7 @@ export const registerRoutes = async ( certificateAuthorityQueue, certificateDAL, certificateBodyDAL, + certificateSecretDAL, pkiCollectionDAL, pkiCollectionItemDAL, projectDAL, diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 42c515795..d026868e9 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -64,7 +64,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); - // TODO(andrey): In the future add support for other formats outside of PEM. Adding a "format" query param may be best. + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. server.route({ method: "GET", url: "/:serialNumber/private-key", @@ -96,7 +96,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { ...req.auditLogInfo, projectId: ca.projectId, event: { - type: EventType.GET_CERT, + type: EventType.GET_CERT_PRIVATE_KEY, metadata: { certId: cert.id, cn: cert.commonName, @@ -109,6 +109,62 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. + server.route({ + method: "GET", + url: "/:serialNumber/bundle", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate bundle including the certificate, chain, and private key.", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), + certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, serialNumber, cert, ca, privateKey } = + await server.services.certificate.getCertBundle({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CERT_BUNDLE, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + return { + certificate, + certificateChain, + serialNumber, + privateKey + }; + } + }); + server.route({ method: "POST", url: "/issue-certificate", @@ -474,7 +530,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { ...req.auditLogInfo, projectId: ca.projectId, event: { - type: EventType.DELETE_CERT, + type: EventType.GET_CERT_BODY, metadata: { certId: cert.id, cn: cert.commonName, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 499a25741..4c40d9ff4 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -21,6 +21,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { CertExtendedKeyUsage, CertExtendedKeyUsageOIDToName, @@ -75,6 +76,7 @@ type TCertificateAuthorityServiceFactoryDep = { certificateTemplateDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; + certificateSecretDAL: Pick; certificateBodyDAL: Pick; pkiCollectionDAL: Pick; pkiCollectionItemDAL: Pick; @@ -96,6 +98,7 @@ export const certificateAuthorityServiceFactory = ({ certificateTemplateDAL, certificateDAL, certificateBodyDAL, + certificateSecretDAL, pkiCollectionDAL, pkiCollectionItemDAL, projectDAL, @@ -1373,6 +1376,23 @@ export const certificateAuthorityServiceFactory = ({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }); + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(skLeaf) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: caCert.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); await certificateDAL.transaction(async (tx) => { const cert = await certificateDAL.create( @@ -1396,7 +1416,16 @@ export const certificateAuthorityServiceFactory = ({ await certificateBodyDAL.create( { certId: cert.id, - encryptedCertificate + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey }, tx ); @@ -1414,17 +1443,9 @@ export const certificateAuthorityServiceFactory = ({ return cert; }); - const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: caCert.id, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - return { certificate: leafCert.toString("pem"), - certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + certificateChain: certificateChainPem, issuingCaCertificate, privateKey: skLeaf, serialNumber, diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 55b731bd7..6f9963db7 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -72,7 +72,6 @@ export const getCertificateCredentials = async ({ projectDAL, kmsService }); - const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: keyId }); @@ -80,7 +79,7 @@ export const getCertificateCredentials = async ({ cipherTextBlob: certificateSecret.encryptedPrivateKey }); - const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" }); + const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" }); const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString(); const pkObj = crypto.createPublicKey(skObj); diff --git a/backend/src/services/certificate/certificate-secret-dal.ts b/backend/src/services/certificate/certificate-secret-dal.ts index d7f3e43ae..c1493eceb 100644 --- a/backend/src/services/certificate/certificate-secret-dal.ts +++ b/backend/src/services/certificate/certificate-secret-dal.ts @@ -5,6 +5,6 @@ import { ormify } from "@app/lib/knex"; export type TCertificateSecretDALFactory = ReturnType; export const certificateSecretDALFactory = (db: TDbClient) => { - const caSecretOrm = ormify(db, TableName.CertificateSecret); - return caSecretOrm; + const certSecretOrm = ormify(db, TableName.CertificateSecret); + return certSecretOrm; }; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index d72235781..3568c0ca1 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -17,7 +17,14 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; -import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; +import { + CertStatus, + TDeleteCertDTO, + TGetCertBodyDTO, + TGetCertBundleDTO, + TGetCertDTO, + TRevokeCertDTO +} from "./certificate-types"; type TCertificateServiceFactoryDep = { certificateDAL: Pick; @@ -86,11 +93,13 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - // TODO(andrey): Update permission for privateKey fetching. Should be very strict. - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificatePrivateKey + ); const { certPrivateKey } = await getCertificateCredentials({ - certId: ca.id, + certId: cert.id, projectId: ca.projectId, certificateSecretDAL, projectDAL, @@ -229,20 +238,110 @@ export const certificateServiceFactory = ({ kmsService }); + let certificateChain = `${caCert}\n${caCertChain}`.trim(); + + // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body + if (certBody.encryptedCertificateChain) { + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificateChain + }); + const certChainObj = new x509.X509Certificate(decryptedCertChain); + certificateChain = certChainObj.toString("pem"); + } + return { certificate: certObj.toString("pem"), - certificateChain: `${caCert}\n${caCertChain}`.trim(), + certificateChain, serialNumber: certObj.serialNumber, cert, ca }; }; + /** + * Return certificate body and certificate chain for certificate with + * serial number [serialNumber] + */ + const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => { + const cert = await certificateDAL.findOne({ serialNumber }); + const ca = await certificateAuthorityDAL.findById(cert.caId); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificatePrivateKey + ); + + const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); + + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKeyId + }); + const decryptedCert = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificate + }); + + const certObj = new x509.X509Certificate(decryptedCert); + const certificate = certObj.toString("pem"); + + const { caCert, caCertChain } = await getCaCertChain({ + caCertId: cert.caCertId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + let certificateChain = `${caCert}\n${caCertChain}`.trim(); + + // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body + if (certBody.encryptedCertificateChain) { + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificateChain + }); + const certChainObj = new x509.X509Certificate(decryptedCertChain); + certificateChain = certChainObj.toString("pem"); + } + + const { certPrivateKey } = await getCertificateCredentials({ + certId: cert.id, + projectId: ca.projectId, + certificateSecretDAL, + projectDAL, + kmsService + }); + + return { + certificate, + certificateChain, + privateKey: certPrivateKey, + serialNumber, + cert, + ca + }; + }; + return { getCert, getCertPrivateKey, deleteCert, revokeCert, - getCertBody + getCertBody, + getCertBundle }; }; diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 48454f803..a36671030 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -78,6 +78,10 @@ export type TGetCertBodyDTO = { serialNumber: string; } & Omit; +export type TGetCertBundleDTO = { + serialNumber: string; +} & Omit; + export type TGetCertificateCredentialsDTO = { certId: string; projectId: string; From c70b9e665e349f942a238578dfcb04d83b194d74 Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 00:39:10 -0400 Subject: [PATCH 03/33] more tweaks and type fix --- .../certificate/certificate-service.ts | 9 ++++++++- .../services/certificate/certificate-types.ts | 4 ++++ .../context/ProjectPermissionContext/types.ts | 1 + .../src/hooks/api/auditLogs/constants.tsx | 2 ++ frontend/src/hooks/api/auditLogs/enums.tsx | 2 ++ frontend/src/hooks/api/auditLogs/types.tsx | 20 +++++++++++++++++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 3568c0ca1..c75b01382 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -23,6 +23,7 @@ import { TGetCertBodyDTO, TGetCertBundleDTO, TGetCertDTO, + TGetCertPrivateKeyDTO, TRevokeCertDTO } from "./certificate-types"; @@ -80,7 +81,13 @@ export const certificateServiceFactory = ({ /** * Get certificate private key. */ - const getCertPrivateKey = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { + const getCertPrivateKey = async ({ + serialNumber, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetCertPrivateKeyDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index a36671030..71a53bd3f 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -78,6 +78,10 @@ export type TGetCertBodyDTO = { serialNumber: string; } & Omit; +export type TGetCertPrivateKeyDTO = { + serialNumber: string; +} & Omit; + export type TGetCertBundleDTO = { serialNumber: string; } & Omit; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index f7ba69e17..d2463d766 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -170,6 +170,7 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", + CertificatePrivateKey = "certificate-private-key", CertificateTemplates = "certificate-templates", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificateTemplates = "ssh-certificate-templates", diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 229f822da..0c1c986a0 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -72,6 +72,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.DELETE_CERT]: "Delete certificate", [EventType.REVOKE_CERT]: "Revoke certificate", [EventType.GET_CERT_BODY]: "Get certificate body", + [EventType.GET_CERT_PRIVATE_KEY]: "Get certificate private key", + [EventType.GET_CERT_BUNDLE]: "Get certificate bundle", [EventType.CREATE_PKI_ALERT]: "Create PKI alert", [EventType.GET_PKI_ALERT]: "Get PKI alert", [EventType.UPDATE_PKI_ALERT]: "Update PKI alert", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index d465fb820..74c159aed 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -78,6 +78,8 @@ export enum EventType { DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", GET_CERT_BODY = "get-cert-body", + GET_CERT_PRIVATE_KEY = "get-cert-private-key", + GET_CERT_BUNDLE = "get-cert-bundle", CREATE_PKI_ALERT = "create-pki-alert", GET_PKI_ALERT = "get-pki-alert", UPDATE_PKI_ALERT = "update-pki-alert", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 2524f84f4..04cd60236 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -619,6 +619,24 @@ interface GetCertBody { }; } +interface GetCertPrivateKey { + type: EventType.GET_CERT_PRIVATE_KEY; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + +interface GetCertBundle { + type: EventType.GET_CERT_BUNDLE; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + interface CreatePkiAlert { type: EventType.CREATE_PKI_ALERT; metadata: { @@ -878,6 +896,8 @@ export type Event = | DeleteCert | RevokeCert | GetCertBody + | GetCertPrivateKey + | GetCertBundle | CreatePkiAlert | GetPkiAlert | UpdatePkiAlert From 9f487ad026603a284865be5c75d7c8afc2523439 Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 00:53:31 -0400 Subject: [PATCH 04/33] frontend type fixes --- backend/src/server/routes/index.ts | 2 +- .../services/secret-v2-bridge/secret-v2-bridge-dal.ts | 2 +- .../src/context/ProjectPermissionContext/types.ts | 1 + frontend/src/layouts/ProjectLayout/ProjectLayout.tsx | 2 +- .../components/ProjectRoleModifySection.utils.tsx | 11 +++++++++++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f9a2223ee..dd864412e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -123,6 +123,7 @@ import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal"; import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { certificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { certificateServiceFactory } from "@app/services/certificate/certificate-service"; import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; @@ -260,7 +261,6 @@ import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; -import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 6ab348520..cd2773172 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -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() { diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d2463d766..67ea65df4 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -270,6 +270,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] + | [ProjectPermissionActions, ProjectPermissionSub.CertificatePrivateKey] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index a862c31a3..8533d7007 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -13,9 +13,9 @@ import { TBreadcrumbFormat } from "@app/components/v2"; import { - useProjectPermission, ProjectPermissionActions, ProjectPermissionSub, + useProjectPermission, useSubscription, useWorkspace } from "@app/context"; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index de2295940..b68ee332f 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -223,6 +223,7 @@ export const projectRoleFormSchema = z.object({ [ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), + [ProjectPermissionSub.CertificatePrivateKey]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default( [] ), @@ -374,6 +375,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, ProjectPermissionSub.CertificateTemplates, + ProjectPermissionSub.CertificatePrivateKey, ProjectPermissionSub.SecretApproval, ProjectPermissionSub.Tags, ProjectPermissionSub.SecretRotation, @@ -1018,6 +1020,15 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, + [ProjectPermissionSub.CertificatePrivateKey]: { + title: "Certificate Private Key", + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, [ProjectPermissionSub.CertificateTemplates]: { title: "Certificate Templates", actions: [ From eedffffc3880b49b89ef9e99155c0a3507b7578e Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 02:07:07 -0400 Subject: [PATCH 05/33] review fixes --- ...9232917_store-cert-secret-key-and-chain.ts | 2 +- .../server/routes/v1/certificate-router.ts | 19 +++++-- .../services/certificate/certificate-fns.ts | 49 ++++++++++++++----- .../certificate/certificate-service.ts | 36 ++++++-------- .../services/certificate/certificate-types.ts | 8 +++ 5 files changed, 78 insertions(+), 36 deletions(-) diff --git a/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts index 2062f9a38..cb5e44a03 100644 --- a/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts +++ b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts @@ -21,7 +21,7 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - if (await knex.schema.hasTable(TableName.Certificate)) { + if (await knex.schema.hasTable(TableName.CertificateSecret)) { await knex.schema.dropTable(TableName.CertificateSecret); } diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index d026868e9..ecbb02734 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ import { z } from "zod"; import { CertificatesSchema } from "@app/db/schemas"; @@ -83,7 +84,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { 200: z.string().trim() } }, - handler: async (req) => { + handler: async (req, reply) => { const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({ serialNumber: req.params.serialNumber, actor: req.permission.type, @@ -105,6 +106,12 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + // Prevent proxies from caching sensitive data (private key) + reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); + reply.header("Pragma", "no-cache"); + reply.header("Expires", "0"); + reply.header("Surrogate-Control", "no-store"); + return certPrivateKey; } }); @@ -128,12 +135,12 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { 200: z.object({ certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), - privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey), serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) }) } }, - handler: async (req) => { + handler: async (req, reply) => { const { certificate, certificateChain, serialNumber, cert, ca, privateKey } = await server.services.certificate.getCertBundle({ serialNumber: req.params.serialNumber, @@ -156,6 +163,12 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + // Prevent proxies from caching sensitive data (private key) + reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); + reply.header("Pragma", "no-cache"); + reply.header("Expires", "0"); + reply.header("Surrogate-Control", "no-store"); + return { certificate, certificateChain, diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 6f9963db7..5cd4929bb 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -2,10 +2,10 @@ import crypto from "node:crypto"; import * as x509 from "@peculiar/x509"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { getProjectKmsCertificateKeyId } from "../project/project-fns"; -import { CrlReason, TGetCertificateCredentialsDTO } from "./certificate-types"; +import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types"; export const revocationReasonToCrlCode = (crlReason: CrlReason) => { switch (crlReason) { @@ -79,15 +79,42 @@ export const getCertificateCredentials = async ({ cipherTextBlob: certificateSecret.encryptedPrivateKey }); - const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" }); - const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString(); + try { + const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" }); + const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString(); - const pkObj = crypto.createPublicKey(skObj); - const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString(); + const pkObj = crypto.createPublicKey(skObj); + const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString(); - return { - certificateSecret, - certPrivateKey, - certPublicKey - }; + return { + certificateSecret, + certPrivateKey, + certPublicKey + }; + } catch (error) { + throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` }); + } +}; + +// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body +// Otherwise we'll fallback to manually building the chain +export const buildCertificateChain = async ({ + caCert, + caCertChain, + encryptedCertificateChain, + kmsService, + kmsId +}: TBuildCertificateChainDTO) => { + let certificateChain = `${caCert}\n${caCertChain}`.trim(); + + // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body + if (encryptedCertificateChain) { + const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId }); + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: encryptedCertificateChain + }); + certificateChain = decryptedCertChain.toString(); + } + + return certificateChain; }; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index c75b01382..d07027fd8 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -15,7 +15,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; -import { getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; +import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; import { CertStatus, @@ -245,16 +245,13 @@ export const certificateServiceFactory = ({ kmsService }); - let certificateChain = `${caCert}\n${caCertChain}`.trim(); - - // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body - if (certBody.encryptedCertificateChain) { - const decryptedCertChain = await kmsDecryptor({ - cipherTextBlob: certBody.encryptedCertificateChain - }); - const certChainObj = new x509.X509Certificate(decryptedCertChain); - certificateChain = certChainObj.toString("pem"); - } + const certificateChain = await buildCertificateChain({ + caCert, + caCertChain, + kmsId: certificateManagerKeyId, + kmsService, + encryptedCertificateChain: certBody.encryptedCertificateChain || undefined + }); return { certificate: certObj.toString("pem"), @@ -314,16 +311,13 @@ export const certificateServiceFactory = ({ kmsService }); - let certificateChain = `${caCert}\n${caCertChain}`.trim(); - - // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body - if (certBody.encryptedCertificateChain) { - const decryptedCertChain = await kmsDecryptor({ - cipherTextBlob: certBody.encryptedCertificateChain - }); - const certChainObj = new x509.X509Certificate(decryptedCertChain); - certificateChain = certChainObj.toString("pem"); - } + const certificateChain = await buildCertificateChain({ + caCert, + caCertChain, + kmsId: certificateManagerKeyId, + kmsService, + encryptedCertificateChain: certBody.encryptedCertificateChain || undefined + }); const { certPrivateKey } = await getCertificateCredentials({ certId: cert.id, diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 71a53bd3f..373fa028c 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -93,3 +93,11 @@ export type TGetCertificateCredentialsDTO = { projectDAL: Pick; kmsService: Pick; }; + +export type TBuildCertificateChainDTO = { + caCert: string; + caCertChain: string; + encryptedCertificateChain?: Buffer; + kmsService: Pick; + kmsId: string; +}; From 235be96ded7a17ef423579cb7f8539bfe7e8fdcc Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 14:53:57 -0400 Subject: [PATCH 06/33] tweaks --- backend/src/services/certificate/certificate-fns.ts | 5 ++++- backend/src/services/certificate/certificate-types.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 5cd4929bb..961fb27ff 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -105,9 +105,12 @@ export const buildCertificateChain = async ({ kmsService, kmsId }: TBuildCertificateChainDTO) => { + if (!encryptedCertificateChain && (!caCert || !caCertChain)) { + return null; + } + let certificateChain = `${caCert}\n${caCertChain}`.trim(); - // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body if (encryptedCertificateChain) { const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId }); const decryptedCertChain = await kmsDecryptor({ diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 373fa028c..ae04eae6b 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -95,8 +95,8 @@ export type TGetCertificateCredentialsDTO = { }; export type TBuildCertificateChainDTO = { - caCert: string; - caCertChain: string; + caCert?: string; + caCertChain?: string; encryptedCertificateChain?: Buffer; kmsService: Pick; kmsId: string; From 07e4bc8eed9f68a2afa3872c9777702d143d3e40 Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 17:46:05 -0400 Subject: [PATCH 07/33] review fixes --- .../services/permission/project-permission.ts | 41 +++++++++------ backend/src/server/lib/caching.ts | 8 +++ .../server/routes/v1/certificate-router.ts | 17 ++----- .../certificate-authority-service.ts | 13 +++-- .../certificate/certificate-service.ts | 38 ++++++++++---- .../src/services/project/project-service.ts | 6 ++- .../ProjectPermissionContext/index.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 12 +++-- frontend/src/context/index.tsx | 1 + .../CertificatesPage/CertificatesPage.tsx | 11 ++-- .../components/CertificatesSection.tsx | 8 ++- .../components/CertificatesTable.tsx | 14 +++-- .../ProjectRoleModifySection.utils.tsx | 51 ++++++++++++------- 13 files changed, 149 insertions(+), 72 deletions(-) create mode 100644 backend/src/server/lib/caching.ts diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7d659e99b..1dbc5ee2a 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -17,6 +17,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionCertificateActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ReadPrivateKey = "read-private-key" +} + export enum ProjectPermissionSecretActions { DescribeAndReadValue = "read", DescribeSecret = "describeSecret", @@ -129,7 +137,6 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", - CertificatePrivateKey = "certificate-private-key", CertificateTemplates = "certificate-templates", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificates = "ssh-certificates", @@ -232,8 +239,7 @@ export type ProjectPermissionSet = ProjectPermissionSub.Identity | (ForcedSubject & IdentityManagementSubjectFields) ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] - | [ProjectPermissionActions, ProjectPermissionSub.Certificates] - | [ProjectPermissionActions, ProjectPermissionSub.CertificatePrivateKey] + | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] @@ -482,12 +488,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.CertificatePrivateKey).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( - "Describe what action an entity can take." - ) - }), z.object({ subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -688,8 +688,6 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.AuditLogs, ProjectPermissionSub.IpAllowList, ProjectPermissionSub.CertificateAuthorities, - ProjectPermissionSub.Certificates, - ProjectPermissionSub.CertificatePrivateKey, ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, @@ -708,6 +706,17 @@ const buildAdminPermissionRules = () => { ); }); + can( + [ + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.Edit, + ProjectPermissionCertificateActions.Create, + ProjectPermissionCertificateActions.Delete, + ProjectPermissionCertificateActions.ReadPrivateKey + ], + ProjectPermissionSub.Certificates + ); + can( [ ProjectPermissionSshHostActions.Edit, @@ -965,10 +974,10 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.Edit, + ProjectPermissionCertificateActions.Create, + ProjectPermissionCertificateActions.Delete ], ProjectPermissionSub.Certificates ); @@ -1041,7 +1050,7 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); diff --git a/backend/src/server/lib/caching.ts b/backend/src/server/lib/caching.ts new file mode 100644 index 000000000..513f2f635 --- /dev/null +++ b/backend/src/server/lib/caching.ts @@ -0,0 +1,8 @@ +import { FastifyReply } from "fastify"; + +export const addNoCacheHeaders = (reply: FastifyReply) => { + void reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); + void reply.header("Pragma", "no-cache"); + void reply.header("Expires", "0"); + void reply.header("Surrogate-Control", "no-store"); +}; diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index ecbb02734..dad1d9a80 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -6,6 +6,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { addNoCacheHeaders } from "@app/server/lib/caching"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -106,11 +107,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); - // Prevent proxies from caching sensitive data (private key) - reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); - reply.header("Pragma", "no-cache"); - reply.header("Expires", "0"); - reply.header("Surrogate-Control", "no-store"); + addNoCacheHeaders(reply); return certPrivateKey; } @@ -134,7 +131,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), - certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain), privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey), serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) }) @@ -163,11 +160,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); - // Prevent proxies from caching sensitive data (private key) - reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); - reply.header("Pragma", "no-cache"); - reply.header("Expires", "0"); - reply.header("Surrogate-Control", "no-store"); + addNoCacheHeaders(reply); return { certificate, @@ -525,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), - certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain), serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) }) } diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 4c40d9ff4..e1d7ce5cb 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -6,7 +6,11 @@ import { z } from "zod"; import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionCertificateActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -1160,7 +1164,10 @@ export const certificateAuthorityServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Create, + ProjectPermissionSub.Certificates + ); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); @@ -1508,7 +1515,7 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionCertificateActions.Create, ProjectPermissionSub.Certificates ); } diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index d07027fd8..73a8caed7 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509"; import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; @@ -70,7 +73,10 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); return { cert, @@ -101,8 +107,8 @@ export const certificateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificatePrivateKey + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates ); const { certPrivateKey } = await getCertificateCredentials({ @@ -136,7 +142,10 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Delete, + ProjectPermissionSub.Certificates + ); const deletedCert = await certificateDAL.deleteById(cert.id); @@ -171,7 +180,10 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Delete, + ProjectPermissionSub.Certificates + ); if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked"); @@ -218,7 +230,10 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); @@ -279,10 +294,13 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificatePrivateKey + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates ); const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 9f2de8a85..db67f49d3 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -14,6 +14,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/servi import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, + ProjectPermissionCertificateActions, ProjectPermissionSecretActions, ProjectPermissionSshHostActions, ProjectPermissionSub @@ -927,7 +928,10 @@ export const projectServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); const cas = await certificateAuthorityDAL.find({ projectId }); diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index 5bc163817..b195571f8 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -2,6 +2,7 @@ export { useProjectPermission } from "./ProjectPermissionContext"; export type { ProjectPermissionSet, TProjectPermission } from "./types"; export { ProjectPermissionActions, + ProjectPermissionCertificateActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, ProjectPermissionGroupActions, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 67ea65df4..5aee9c483 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -7,6 +7,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionCertificateActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ReadPrivateKey = "read-private-key" +} + export enum ProjectPermissionSecretActions { DescribeAndReadValue = "read", DescribeSecret = "describeSecret", @@ -170,7 +178,6 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", - CertificatePrivateKey = "certificate-private-key", CertificateTemplates = "certificate-templates", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificateTemplates = "ssh-certificate-templates", @@ -268,9 +275,8 @@ export type ProjectPermissionSet = ) ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] - | [ProjectPermissionActions, ProjectPermissionSub.Certificates] + | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] - | [ProjectPermissionActions, ProjectPermissionSub.CertificatePrivateKey] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 51f2797d0..04af3c8a4 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -10,6 +10,7 @@ export { export type { TProjectPermission } from "./ProjectPermissionContext"; export { ProjectPermissionActions, + ProjectPermissionCertificateActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, ProjectPermissionGroupActions, diff --git a/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx b/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx index c985f313f..4a4a22093 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx @@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useProjectPermission +} from "@app/context"; import { PkiCollectionSection } from "../AlertingPage/components"; import { CertificatesSection } from "./components"; @@ -17,7 +22,7 @@ export const CertificatesPage = () => { ProjectPermissionSub.PkiCollections ); const canAccessCerts = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates ); @@ -40,7 +45,7 @@ export const CertificatesPage = () => { )} diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index d5f94e7b7..ef8b09dac 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; import { useDeleteCert } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -50,7 +54,7 @@ export const CertificatesSection = () => {

Certificates

{(isAllowed) => ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index dcc832bfb..8cf7dda24 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -30,7 +30,11 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; import { useListWorkspaceCertificates } from "@app/hooks/api"; import { CertStatus } from "@app/hooks/api/certificates/enums"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -110,7 +114,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { {(isAllowed) => ( @@ -131,7 +135,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( @@ -152,7 +156,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( @@ -173,7 +177,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index b68ee332f..6b90b791d 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -6,6 +6,7 @@ import { z } from "zod"; import { Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, + ProjectPermissionCertificateActions, ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/context"; @@ -32,6 +33,14 @@ const GeneralPolicyActionSchema = z.object({ create: z.boolean().optional() }); +const CertificatePolicyActionSchema = z.object({ + [ProjectPermissionCertificateActions.Create]: z.boolean().optional(), + [ProjectPermissionCertificateActions.Delete]: z.boolean().optional(), + [ProjectPermissionCertificateActions.Edit]: z.boolean().optional(), + [ProjectPermissionCertificateActions.Read]: z.boolean().optional(), + [ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional() +}); + const SecretPolicyActionSchema = z.object({ [ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), @@ -219,11 +228,10 @@ export const projectRoleFormSchema = z.object({ [ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]), - [ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]), + [ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), - [ProjectPermissionSub.CertificatePrivateKey]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default( [] ), @@ -371,11 +379,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.AuditLogs, ProjectPermissionSub.IpAllowList, ProjectPermissionSub.CertificateAuthorities, - ProjectPermissionSub.Certificates, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, ProjectPermissionSub.CertificateTemplates, - ProjectPermissionSub.CertificatePrivateKey, ProjectPermissionSub.SecretApproval, ProjectPermissionSub.Tags, ProjectPermissionSub.SecretRotation, @@ -507,6 +513,25 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { return; } + if (subject === ProjectPermissionSub.Certificates) { + const canRead = action.includes(ProjectPermissionCertificateActions.Read); + const canEdit = action.includes(ProjectPermissionCertificateActions.Edit); + const canDelete = action.includes(ProjectPermissionCertificateActions.Delete); + const canCreate = action.includes(ProjectPermissionCertificateActions.Create); + const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey); + + if (!formVal[subject]) formVal[subject] = [{}]; + + // from above statement we are sure it won't be undefined + if (canRead) formVal[subject]![0].read = true; + if (canEdit) formVal[subject]![0].edit = true; + if (canCreate) formVal[subject]![0].create = true; + if (canDelete) formVal[subject]![0].delete = true; + if (canReadPrivateKey) + formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true; + return; + } + if (subject === ProjectPermissionSub.Project) { const canEdit = action.includes(ProjectPermissionActions.Edit); const canDelete = action.includes(ProjectPermissionActions.Delete); @@ -1014,19 +1039,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Certificates]: { title: "Certificates", actions: [ - { label: "Read", value: "read" }, - { label: "Create", value: "create" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } - ] - }, - [ProjectPermissionSub.CertificatePrivateKey]: { - title: "Certificate Private Key", - actions: [ - { label: "Read", value: "read" }, - { label: "Create", value: "create" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { label: "Read", value: ProjectPermissionCertificateActions.Read }, + { label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey }, + { label: "Create", value: ProjectPermissionCertificateActions.Create }, + { label: "Modify", value: ProjectPermissionCertificateActions.Edit }, + { label: "Remove", value: ProjectPermissionCertificateActions.Delete } ] }, [ProjectPermissionSub.CertificateTemplates]: { From 1268bc12388874a8995422cfc3fc872c9726e2dd Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 17:50:23 -0400 Subject: [PATCH 08/33] coderabbit review fixes --- backend/src/ee/services/permission/project-permission.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 1dbc5ee2a..03c8ed61a 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -484,7 +484,7 @@ const GeneralPermissionSchema = [ }), z.object({ subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe( "Describe what action an entity can take." ) }), From 82c7dad6c8ac5d573eb396a379cd212ff781fa0f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 1 May 2025 06:45:40 +0400 Subject: [PATCH 09/33] feat(helm): infisical helm --- .github/workflows/release_docker_gateway.yaml | 44 +++++++++++++ .github/workflows/release_helm_gateway.yaml | 27 ++++++++ Dockerfile.gateway | 24 +++++++ helm-charts/infisical-gateway/.helmignore | 23 +++++++ helm-charts/infisical-gateway/CHANGELOG.md | 3 + helm-charts/infisical-gateway/Chart.yaml | 24 +++++++ .../infisical-gateway/templates/_helpers.tpl | 62 ++++++++++++++++++ .../templates/deployment.yaml | 64 +++++++++++++++++++ .../infisical-gateway/templates/service.yaml | 15 +++++ .../templates/serviceaccount.yaml | 13 ++++ helm-charts/infisical-gateway/values.yaml | 41 ++++++++++++ helm-charts/upload-gateway-cloudsmith.sh | 8 +++ 12 files changed, 348 insertions(+) create mode 100644 .github/workflows/release_docker_gateway.yaml create mode 100644 .github/workflows/release_helm_gateway.yaml create mode 100644 Dockerfile.gateway create mode 100644 helm-charts/infisical-gateway/.helmignore create mode 100644 helm-charts/infisical-gateway/CHANGELOG.md create mode 100644 helm-charts/infisical-gateway/Chart.yaml create mode 100644 helm-charts/infisical-gateway/templates/_helpers.tpl create mode 100644 helm-charts/infisical-gateway/templates/deployment.yaml create mode 100644 helm-charts/infisical-gateway/templates/service.yaml create mode 100644 helm-charts/infisical-gateway/templates/serviceaccount.yaml create mode 100644 helm-charts/infisical-gateway/values.yaml create mode 100755 helm-charts/upload-gateway-cloudsmith.sh diff --git a/.github/workflows/release_docker_gateway.yaml b/.github/workflows/release_docker_gateway.yaml new file mode 100644 index 000000000..666b2c46c --- /dev/null +++ b/.github/workflows/release_docker_gateway.yaml @@ -0,0 +1,44 @@ +name: Release Gateway Docker Image +on: + push: + tags: + - "infisical-gateway/v*.*.*" + +permissions: + contents: write + pull-requests: write + +jobs: + release-image: + name: Release Gateway Docker Image + runs-on: ubuntu-latest + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-gateway/}" + + - name: Checkout code + uses: actions/checkout@v2 + + - name: 🔧 Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + infisical/gateway:latest + infisical/gateway:${{ steps.extract_version.outputs.version }} diff --git a/.github/workflows/release_helm_gateway.yaml b/.github/workflows/release_helm_gateway.yaml new file mode 100644 index 000000000..466849a2d --- /dev/null +++ b/.github/workflows/release_helm_gateway.yaml @@ -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@v2 + + - 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 }} diff --git a/Dockerfile.gateway b/Dockerfile.gateway new file mode 100644 index 000000000..dd4b3f0fe --- /dev/null +++ b/Dockerfile.gateway @@ -0,0 +1,24 @@ +# Build stage +FROM golang:1.23.0-alpine AS builder + +WORKDIR /app + +COPY ./cli . + +RUN go build -o infisical-cli . + +FROM alpine:latest + +ENV TOKEN="" +ENV INFISICAL_API_URL="https://app.infisical.com" + + + +WORKDIR /app + +COPY --from=builder /app/infisical-cli . + +# Make the binary executable +RUN chmod +x /app/infisical-cli + +CMD ["sh", "-c", "if [ -z \"${TOKEN}\" ]; then echo 'ERROR: TOKEN environment variable must be set'; exit 1; fi && ./infisical-cli gateway --token ${TOKEN} --domain ${INFISICAL_API_URL}"] \ No newline at end of file diff --git a/helm-charts/infisical-gateway/.helmignore b/helm-charts/infisical-gateway/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/helm-charts/infisical-gateway/.helmignore @@ -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/ diff --git a/helm-charts/infisical-gateway/CHANGELOG.md b/helm-charts/infisical-gateway/CHANGELOG.md new file mode 100644 index 000000000..832423082 --- /dev/null +++ b/helm-charts/infisical-gateway/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 (May 1, 2025) + +* Initial helm release \ No newline at end of file diff --git a/helm-charts/infisical-gateway/Chart.yaml b/helm-charts/infisical-gateway/Chart.yaml new file mode 100644 index 000000000..382acd3e1 --- /dev/null +++ b/helm-charts/infisical-gateway/Chart.yaml @@ -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" diff --git a/helm-charts/infisical-gateway/templates/_helpers.tpl b/helm-charts/infisical-gateway/templates/_helpers.tpl new file mode 100644 index 000000000..b983e79df --- /dev/null +++ b/helm-charts/infisical-gateway/templates/_helpers.tpl @@ -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 }} diff --git a/helm-charts/infisical-gateway/templates/deployment.yaml b/helm-charts/infisical-gateway/templates/deployment.yaml new file mode 100644 index 000000000..99b107ef5 --- /dev/null +++ b/helm-charts/infisical-gateway/templates/deployment.yaml @@ -0,0 +1,64 @@ +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: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- range $key, $value := .Values.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + 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 }} diff --git a/helm-charts/infisical-gateway/templates/service.yaml b/helm-charts/infisical-gateway/templates/service.yaml new file mode 100644 index 000000000..c1457f80e --- /dev/null +++ b/helm-charts/infisical-gateway/templates/service.yaml @@ -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 }} diff --git a/helm-charts/infisical-gateway/templates/serviceaccount.yaml b/helm-charts/infisical-gateway/templates/serviceaccount.yaml new file mode 100644 index 000000000..c0040963f --- /dev/null +++ b/helm-charts/infisical-gateway/templates/serviceaccount.yaml @@ -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 }} diff --git a/helm-charts/infisical-gateway/values.yaml b/helm-charts/infisical-gateway/values.yaml new file mode 100644 index 000000000..e2bb8e9ad --- /dev/null +++ b/helm-charts/infisical-gateway/values.yaml @@ -0,0 +1,41 @@ +image: + repository: infisical/infisical-gateway + pullPolicy: IfNotPresent + tag: "latest" + +# Environment variables for the gateway +env: + TOKEN: "" + INFISICAL_API_URL: "https://app.infisical.com/api" + +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "" + +podAnnotations: {} +podLabels: {} +podSecurityContext: {} +securityContext: {} +affinity: {} +tolerations: {} +nodeSelector: {} +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false diff --git a/helm-charts/upload-gateway-cloudsmith.sh b/helm-charts/upload-gateway-cloudsmith.sh new file mode 100755 index 000000000..0d6fd11c4 --- /dev/null +++ b/helm-charts/upload-gateway-cloudsmith.sh @@ -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 .. \ No newline at end of file From 835b2fba9c7beb45221962f0b5615a516a3548e9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 1 May 2025 18:02:27 +0400 Subject: [PATCH 10/33] requested changes --- .github/workflows/release_docker_gateway.yaml | 3 +-- .github/workflows/release_helm_gateway.yaml | 2 +- Dockerfile.gateway | 2 +- .../templates/deployment.yaml | 8 +++--- .../infisical-gateway/templates/secret.yaml | 13 ++++++++++ helm-charts/infisical-gateway/values.yaml | 26 +++++++++++++------ 6 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 helm-charts/infisical-gateway/templates/secret.yaml diff --git a/.github/workflows/release_docker_gateway.yaml b/.github/workflows/release_docker_gateway.yaml index 666b2c46c..da1e105e3 100644 --- a/.github/workflows/release_docker_gateway.yaml +++ b/.github/workflows/release_docker_gateway.yaml @@ -6,7 +6,6 @@ on: permissions: contents: write - pull-requests: write jobs: release-image: @@ -18,7 +17,7 @@ jobs: run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-gateway/}" - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: 🔧 Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/release_helm_gateway.yaml b/.github/workflows/release_helm_gateway.yaml index 466849a2d..1b067bd73 100644 --- a/.github/workflows/release_helm_gateway.yaml +++ b/.github/workflows/release_helm_gateway.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Helm uses: azure/setup-helm@v3 diff --git a/Dockerfile.gateway b/Dockerfile.gateway index dd4b3f0fe..24c03e3e0 100644 --- a/Dockerfile.gateway +++ b/Dockerfile.gateway @@ -7,7 +7,7 @@ COPY ./cli . RUN go build -o infisical-cli . -FROM alpine:latest +FROM alpine:3.19 ENV TOKEN="" ENV INFISICAL_API_URL="https://app.infisical.com" diff --git a/helm-charts/infisical-gateway/templates/deployment.yaml b/helm-charts/infisical-gateway/templates/deployment.yaml index 99b107ef5..636e88d35 100644 --- a/helm-charts/infisical-gateway/templates/deployment.yaml +++ b/helm-charts/infisical-gateway/templates/deployment.yaml @@ -37,11 +37,9 @@ spec: {{- end }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - {{- range $key, $value := .Values.env }} - - name: {{ $key }} - value: {{ $value | quote }} - {{- end }} + envFrom: + - secretRef: + name: {{ .Values.secret.name }} ports: - name: http containerPort: {{ .Values.service.port }} diff --git a/helm-charts/infisical-gateway/templates/secret.yaml b/helm-charts/infisical-gateway/templates/secret.yaml new file mode 100644 index 000000000..f186b8ec4 --- /dev/null +++ b/helm-charts/infisical-gateway/templates/secret.yaml @@ -0,0 +1,13 @@ +{{- if .Values.secret.create -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.secret.name }} + labels: + {{- include "infisical-gateway.labels" . | nindent 4 }} +type: Opaque +data: + {{- range $key, $value := .Values.secret.data }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-charts/infisical-gateway/values.yaml b/helm-charts/infisical-gateway/values.yaml index e2bb8e9ad..bb63a82a8 100644 --- a/helm-charts/infisical-gateway/values.yaml +++ b/helm-charts/infisical-gateway/values.yaml @@ -1,16 +1,22 @@ image: repository: infisical/infisical-gateway pullPolicy: IfNotPresent - tag: "latest" + tag: "v0.0.1" -# Environment variables for the gateway -env: - TOKEN: "" - INFISICAL_API_URL: "https://app.infisical.com/api" +secret: + # create a new secret (if true) or use existing (if false) + create: true + # name of the secret to use/create + name: "infisical-gateway-secrets" + + # Secret data (only used if create: true) + data: + TOKEN: "" + INFISICAL_API_URL: "https://app.infisical.com" resources: limits: - cpu: 100m + cpu: 500m memory: 128Mi requests: cpu: 100m @@ -28,8 +34,12 @@ serviceAccount: podAnnotations: {} podLabels: {} -podSecurityContext: {} -securityContext: {} +podSecurityContext: + runAsNonRoot: true + +securityContext: + runAsNonRoot: true + affinity: {} tolerations: {} nodeSelector: {} From 66fbcc68065cd1cc8a4ec22fb74cb8568b66d879 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 1 May 2025 14:57:24 -0700 Subject: [PATCH 11/33] improvemet(email-templates): migrate email templates to react email --- backend/.eslintrc.js | 9 + backend/package-lock.json | 2493 ++++++++++++++++- backend/package.json | 8 +- .../secret-rotation-v2-fns.ts | 2 +- .../ssh/ssh-certificate-authority-service.ts | 4 +- .../src/services/auth/auth-login-service.ts | 6 +- backend/src/services/org/org-service.ts | 12 +- .../services/secret-sync/secret-sync-fns.ts | 2 +- .../services/secret-sync/secret-sync-queue.ts | 2 +- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 2 +- backend/src/services/secret/secret-queue.ts | 2 +- .../emails/AccessApprovalRequestTemplate.tsx | 95 + .../services/smtp/emails/BaseEmailWrapper.tsx | 45 + .../services/smtp/emails/EmailMfaTemplate.tsx | 50 + .../smtp/emails/EmailVerificationTemplate.tsx | 53 + .../emails/ExternalImportFailedTemplate.tsx | 43 + .../emails/ExternalImportStartedTemplate.tsx | 30 + .../ExternalImportSucceededTemplate.tsx | 31 + .../emails/IntegrationSyncFailedTemplate.tsx | 65 + .../smtp/emails/NewDeviceLoginTemplate.tsx | 68 + .../OrgAdminBreakglassAccessTemplate.tsx | 57 + .../OrgAdminProjectGrantAccessTemplate.tsx | 40 + .../emails/OrganizationInvitationTemplate.tsx | 75 + .../smtp/emails/PasswordResetTemplate.tsx | 58 + .../smtp/emails/PasswordSetupTemplate.tsx | 57 + .../emails/PkiExpirationAlertTemplate.tsx | 68 + .../emails/ProjectAccessRequestTemplate.tsx | 68 + .../smtp/emails/ProjectInvitationTemplate.tsx | 50 + .../emails/ScimUserProvisionedTemplate.tsx | 56 + .../SecretApprovalRequestBypassedTemplate.tsx | 72 + ...cretApprovalRequestNeedsReviewTemplate.tsx | 57 + .../emails/SecretLeakIncidentTemplate.tsx | 82 + .../smtp/emails/SecretReminderTemplate.tsx | 45 + .../emails/SecretRequestCompletedTemplate.tsx | 53 + .../emails/SecretRotationFailedTemplate.tsx | 68 + .../smtp/emails/SecretSyncFailedTemplate.tsx | 80 + .../ServiceTokenExpiryNoticeTemplate.tsx | 53 + .../SignupEmailVerificationTemplate.tsx | 53 + .../smtp/emails/UnlockAccountTemplate.tsx | 46 + backend/src/services/smtp/emails/index.ts | 28 + backend/src/services/smtp/smtp-service.ts | 112 - backend/src/services/smtp/smtp-service.tsx | 179 ++ .../accessApprovalRequest.handlebars | 55 - .../accessSecretRequestBypassed.handlebars | 33 - .../smtp/templates/emailMfa.handlebars | 20 - .../templates/emailVerification.handlebars | 17 - .../historicalSecretLeakIncident.handlebars | 21 - .../integrationSyncFailed.handlebars | 33 - .../smtp/templates/newDevice.handlebars | 22 - .../orgAdminBreakglassAccess.handlebars | 20 - .../orgAdminProjectGrantAccess.handlebars | 16 - .../organizationInvitation.handlebars | 18 - .../smtp/templates/passwordReset.handlebars | 16 - .../smtp/templates/passwordSetup.handlebars | 17 - .../templates/pkiExpirationAlert.handlebars | 33 - .../smtp/templates/projectAccess.handlebars | 26 - .../templates/scimUserProvisioned.handlebars | 18 - ...ecretApprovalRequestNeedsReview.handlebars | 24 - .../templates/secretLeakIncident.handlebars | 27 - .../smtp/templates/secretReminder.handlebars | 20 - .../secretRequestCompleted.handlebars | 33 - .../templates/secretRotationFailed.handlebars | 31 - .../templates/secretSyncFailed.handlebars | 39 - .../templates/serviceTokenExpired.handlebars | 19 - .../signupEmailVerification.handlebars | 19 - .../smtp/templates/unlockAccount.handlebars | 18 - .../templates/workspaceInvitation.handlebars | 17 - .../super-admin/super-admin-service.ts | 4 +- backend/tsconfig.json | 3 +- 69 files changed, 4224 insertions(+), 874 deletions(-) create mode 100644 backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx create mode 100644 backend/src/services/smtp/emails/BaseEmailWrapper.tsx create mode 100644 backend/src/services/smtp/emails/EmailMfaTemplate.tsx create mode 100644 backend/src/services/smtp/emails/EmailVerificationTemplate.tsx create mode 100644 backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/ExternalImportSucceededTemplate.tsx create mode 100644 backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx create mode 100644 backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx create mode 100644 backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx create mode 100644 backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx create mode 100644 backend/src/services/smtp/emails/PasswordResetTemplate.tsx create mode 100644 backend/src/services/smtp/emails/PasswordSetupTemplate.tsx create mode 100644 backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx create mode 100644 backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx create mode 100644 backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx create mode 100644 backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SecretReminderTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx create mode 100644 backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx create mode 100644 backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx create mode 100644 backend/src/services/smtp/emails/UnlockAccountTemplate.tsx create mode 100644 backend/src/services/smtp/emails/index.ts delete mode 100644 backend/src/services/smtp/smtp-service.ts create mode 100644 backend/src/services/smtp/smtp-service.tsx delete mode 100644 backend/src/services/smtp/templates/accessApprovalRequest.handlebars delete mode 100644 backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars delete mode 100644 backend/src/services/smtp/templates/emailMfa.handlebars delete mode 100644 backend/src/services/smtp/templates/emailVerification.handlebars delete mode 100644 backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars delete mode 100644 backend/src/services/smtp/templates/integrationSyncFailed.handlebars delete mode 100644 backend/src/services/smtp/templates/newDevice.handlebars delete mode 100644 backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars delete mode 100644 backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars delete mode 100644 backend/src/services/smtp/templates/organizationInvitation.handlebars delete mode 100644 backend/src/services/smtp/templates/passwordReset.handlebars delete mode 100644 backend/src/services/smtp/templates/passwordSetup.handlebars delete mode 100644 backend/src/services/smtp/templates/pkiExpirationAlert.handlebars delete mode 100644 backend/src/services/smtp/templates/projectAccess.handlebars delete mode 100644 backend/src/services/smtp/templates/scimUserProvisioned.handlebars delete mode 100644 backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars delete mode 100644 backend/src/services/smtp/templates/secretLeakIncident.handlebars delete mode 100644 backend/src/services/smtp/templates/secretReminder.handlebars delete mode 100644 backend/src/services/smtp/templates/secretRequestCompleted.handlebars delete mode 100644 backend/src/services/smtp/templates/secretRotationFailed.handlebars delete mode 100644 backend/src/services/smtp/templates/secretSyncFailed.handlebars delete mode 100644 backend/src/services/smtp/templates/serviceTokenExpired.handlebars delete mode 100644 backend/src/services/smtp/templates/signupEmailVerification.handlebars delete mode 100644 backend/src/services/smtp/templates/unlockAccount.handlebars delete mode 100644 backend/src/services/smtp/templates/workspaceInvitation.handlebars diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index b23cf05ae..f901f0df9 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -69,6 +69,15 @@ module.exports = { ["^\\."] ] } + ], + "import/extensions": [ + "error", + "ignorePackages", + { + "": "never", // this is required to get the .tsx to work... + ts: "never", + tsx: "never" + } ] } }; diff --git a/backend/package-lock.json b/backend/package-lock.json index 19e10942e..93c28c9e2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -48,6 +48,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", @@ -107,6 +108,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", @@ -142,6 +145,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", @@ -161,6 +165,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", @@ -2902,13 +2907,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -2986,15 +2993,17 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -3010,6 +3019,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@babel/generator/node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", @@ -3348,19 +3370,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -3402,76 +3426,15 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", + "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -4807,33 +4770,32 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", + "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -4874,14 +4836,14 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5192,6 +5154,17 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -5465,6 +5438,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", @@ -5481,6 +5471,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", @@ -6222,6 +6229,386 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@infisical/quic": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@infisical/quic/-/quic-1.0.8.tgz", @@ -6759,6 +7146,149 @@ "win32" ] }, + "node_modules/@next/env": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", + "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", + "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", + "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", + "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", + "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", + "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", + "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", + "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", + "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", @@ -8519,6 +9049,286 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@react-email/body": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz", + "integrity": "sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/button": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz", + "integrity": "sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-block": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.12.tgz", + "integrity": "sha512-Faw3Ij9+/Qwq6moWaeHnV8Hn7ekc/EqyAzPi6yUar21dhcqYugCC4Da1x4d9nA9zC0H9KU3lYVJczh8D3cA+Eg==", + "license": "MIT", + "dependencies": { + "prismjs": "1.30.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-inline": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz", + "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/column": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.13.tgz", + "integrity": "sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/components": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.36.tgz", + "integrity": "sha512-VMh+OQplAnG8JMLlJjdnjt+ThJZ+JVkp0q2YMS2NEz+T88N22bLD2p7DZO0QgtNaKgumOhJI/0a2Q7VzCrwu5g==", + "license": "MIT", + "dependencies": { + "@react-email/body": "0.0.11", + "@react-email/button": "0.0.19", + "@react-email/code-block": "0.0.12", + "@react-email/code-inline": "0.0.5", + "@react-email/column": "0.0.13", + "@react-email/container": "0.0.15", + "@react-email/font": "0.0.9", + "@react-email/head": "0.0.12", + "@react-email/heading": "0.0.15", + "@react-email/hr": "0.0.11", + "@react-email/html": "0.0.11", + "@react-email/img": "0.0.11", + "@react-email/link": "0.0.12", + "@react-email/markdown": "0.0.14", + "@react-email/preview": "0.0.12", + "@react-email/render": "1.0.6", + "@react-email/row": "0.0.12", + "@react-email/section": "0.0.16", + "@react-email/tailwind": "1.0.4", + "@react-email/text": "0.1.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/container": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz", + "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/font": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.9.tgz", + "integrity": "sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/head": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.12.tgz", + "integrity": "sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/heading": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz", + "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/hr": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz", + "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/html": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.11.tgz", + "integrity": "sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/img": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz", + "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/link": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz", + "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/markdown": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.14.tgz", + "integrity": "sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==", + "license": "MIT", + "dependencies": { + "md-to-react-email": "5.0.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/preview": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz", + "integrity": "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/render": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", + "integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==", + "license": "MIT", + "dependencies": { + "html-to-text": "9.0.5", + "prettier": "3.5.3", + "react-promise-suspense": "0.3.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/row": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.12.tgz", + "integrity": "sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/section": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.16.tgz", + "integrity": "sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/tailwind": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.4.tgz", + "integrity": "sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/text": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.1.tgz", + "integrity": "sha512-Zo9tSEzkO3fODLVH1yVhzVCiwETfeEL5wU93jXKWo2DHoMuiZ9Iabaso3T0D0UjhrCB1PBMeq2YiejqeToTyIQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", @@ -8743,6 +9553,19 @@ "win32" ] }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/@sentry-internal/tracing": { "version": "7.119.2", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.119.2.tgz", @@ -9659,6 +10482,13 @@ "node": ">=16.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.3.107", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz", @@ -9870,22 +10700,20 @@ } }, "node_modules/@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true, - "optional": true, - "peer": true + "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", - "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "dev": true, - "optional": true, - "peer": true, + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@swc/types": { @@ -9979,6 +10807,16 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -10287,6 +11125,16 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, + "node_modules/@types/react": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, "node_modules/@types/readable-stream": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", @@ -11914,6 +12762,16 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -12454,6 +13312,18 @@ "esbuild": ">=0.17" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -12794,6 +13664,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -12863,6 +13740,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -13053,6 +13940,20 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -13130,6 +14031,13 @@ "node": ">=18" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -13262,6 +14170,19 @@ "node": ">= 8" } }, + "node_modules/debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz", + "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -13354,6 +14275,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -13465,9 +14399,10 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -13697,6 +14632,77 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -16116,6 +17122,41 @@ ], "license": "MIT" }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/html-to-text/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/htmlparser2": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", @@ -16975,7 +18016,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -17487,6 +18529,15 @@ "integrity": "sha512-8s46m/r2lSFO2+DqMxqWiJ10iiL4tuR5LC/KndV+E5//OAOzOx5s3HS5O34PJ5+kyaCA+K2oCaEPaDRfXUnQow==", "license": "MIT" }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -17872,6 +18923,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/marked": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", + "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -17880,6 +18943,18 @@ "node": ">= 0.4" } }, + "node_modules/md-to-react-email": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.5.tgz", + "integrity": "sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==", + "license": "MIT", + "dependencies": { + "marked": "7.0.4" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -18540,6 +19615,90 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/next": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", + "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/env": "15.2.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.2.4", + "@next/swc-darwin-x64": "15.2.4", + "@next/swc-linux-arm64-gnu": "15.2.4", + "@next/swc-linux-arm64-musl": "15.2.4", + "@next/swc-linux-x64-gnu": "15.2.4", + "@next/swc-linux-x64-musl": "15.2.4", + "@next/swc-win32-arm64-msvc": "15.2.4", + "@next/swc-win32-x64-msvc": "15.2.4", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/node-abi": { "version": "3.65.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", @@ -19557,6 +20716,19 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -19732,6 +20904,15 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/pg": { "version": "8.13.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", @@ -20320,11 +21501,10 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", - "dev": true, - "peer": true, + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -20373,6 +21553,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/probot": { "version": "13.3.8", "resolved": "https://registry.npmjs.org/probot/-/probot-13.3.8.tgz", @@ -20928,12 +22117,817 @@ "node-gyp": "^10.2.0" } }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-email": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.7.tgz", + "integrity": "sha512-XCXlfZLKv9gHd/ZwUEhCpRGc/FJLZGYczeuG1kVR/be2PlkwEB4gjX9ARBbRFv86ncbtpOu/wI6jD6kadRyAKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.5", + "@babel/traverse": "7.25.6", + "chalk": "4.1.2", + "chokidar": "4.0.3", + "commander": "11.1.0", + "debounce": "2.0.0", + "esbuild": "0.25.0", + "glob": "10.3.4", + "log-symbols": "4.1.0", + "mime-types": "2.1.35", + "next": "15.2.4", + "normalize-path": "3.0.0", + "ora": "5.4.1", + "socket.io": "4.8.1" + }, + "bin": { + "email": "dist/cli/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/react-email/node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/react-email/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/react-email/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/react-email/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/react-email/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/react-email/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/react-email/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-email/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/react-email/node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/react-email/node_modules/glob": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", + "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/react-email/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/react-email/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/react-email/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/react-email/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-email/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-email/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-promise-suspense": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", + "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -21509,6 +23503,12 @@ "node": ">=v12.22.7" } }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, "node_modules/scim-patch": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz", @@ -21537,18 +23537,28 @@ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semifies": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -21708,6 +23718,62 @@ "sha.js": "bin.js" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -22080,6 +24146,126 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socks": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", @@ -22326,6 +24512,15 @@ "node": ">=4.0.0" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -22521,6 +24716,30 @@ "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", "license": "MIT" }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -24923,6 +27142,16 @@ "node": ">=18" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 58bc6a32d..c19d30441 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 5c0d97ee8..a25482c8c 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -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."; } diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index 312b7966b..d58644d90 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -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 }); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index d1b0a550d..5e2b450bb 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -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, diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 060a01634..d794391c1 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -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(), diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 143fa4622..1d3e8888c 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -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."; } diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 34ac84947..62b4ba3cc 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -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}` } }); }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 6ab348520..cd2773172 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -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() { diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 6a0868741..714df0d3f 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -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` } }); } diff --git a/backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx b/backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx new file mode 100644 index 000000000..fef072546 --- /dev/null +++ b/backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx @@ -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 { + 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 ( + + + You have a new access approval request pending review for the project {projectName} + +
+ + {requesterFullName} ( + + {requesterEmail} + + ) has requested {isTemporary ? "temporary" : "permanent"} access to {secretPath} in the{" "} + {environment} environment. + + + {isTemporary && ( + + This access will expire {expiresIn} after approval. + + )} + + The following permissions are requested: + + {permissions.map((permission) => ( + + - {permission} + + ))} + {note && ( + + User Note: "{note}" + + )} +
+
+ +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/BaseEmailWrapper.tsx b/backend/src/services/smtp/emails/BaseEmailWrapper.tsx new file mode 100644 index 000000000..3d02fc793 --- /dev/null +++ b/backend/src/services/smtp/emails/BaseEmailWrapper.tsx @@ -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 ( + + + + + {preview} + +
+
+
+ Infisical Logo +
+
+
{children}
+
+
+ + Email sent via{" "} + + Infisical + + +
+ + + + + ); +}; diff --git a/backend/src/services/smtp/emails/EmailMfaTemplate.tsx b/backend/src/services/smtp/emails/EmailMfaTemplate.tsx new file mode 100644 index 000000000..8a35b011d --- /dev/null +++ b/backend/src/services/smtp/emails/EmailMfaTemplate.tsx @@ -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 { + code: string; + isCloud: boolean; +} + +export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplateProps) => { + return ( + + + MFA required + +
+ Enter the MFA code below in the browser where you started sign-in. + + {code} + +
+
+ + Not you?{" "} + {isCloud ? ( + <> + Contact us at{" "} + + support@infisical.com + {" "} + immediately + + ) : ( + "Contact your administrator immediately" + )} + . + +
+
+ ); +}; + +export default EmailMfaTemplate; + +EmailMfaTemplate.PreviewProps = { + code: "124356", + isCloud: true, + siteUrl: "https://infisical.com" +} as EmailMfaTemplateProps; diff --git a/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx b/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx new file mode 100644 index 000000000..6c0f410b1 --- /dev/null +++ b/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx @@ -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 { + code: string; + isCloud: boolean; +} + +export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerificationTemplateProps) => { + return ( + + + Confirm your email address + +
+ Enter the confirmation code below in the browser window requiring confirmation. + + {code} + +
+
+ + Questions about Infisical?{" "} + {isCloud ? ( + <> + Email us at{" "} + + support@infisical.com + + + ) : ( + "Contact your administrator" + )} + . + +
+
+ ); +}; + +export default EmailVerificationTemplate; + +EmailVerificationTemplate.PreviewProps = { + code: "124356", + isCloud: true, + siteUrl: "https://infisical.com" +} as EmailVerificationTemplateProps; diff --git a/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx b/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx new file mode 100644 index 000000000..c283fdad9 --- /dev/null +++ b/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx @@ -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 { + error: string; + provider: string; +} + +export const ExternalImportFailedTemplate = ({ error, siteUrl, provider }: ExternalImportFailedTemplateProps) => { + return ( + + + An import from {provider} to Infisical has failed + +
+ + An import from {provider} to Infisical has failed due to unforeseen circumstances. Please + re-try your import. + + + If your issue persists, you can contact the Infisical team at{" "} + + support@infisical.com + + . + + + Error: "{error}" + +
+
+ ); +}; + +export default ExternalImportFailedTemplate; + +ExternalImportFailedTemplate.PreviewProps = { + provider: "EnvKey", + error: "Something went wrong. Please try again.", + siteUrl: "https://infisical.com" +} as ExternalImportFailedTemplateProps; diff --git a/backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx b/backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx new file mode 100644 index 000000000..fc24536b4 --- /dev/null +++ b/backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx @@ -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 { + provider: string; +} + +export const ExternalImportStartedTemplate = ({ siteUrl, provider }: ExternalImportStartedTemplateProps) => { + return ( + + + An import from {provider} to Infisical has been started + +
+ + An import from {provider} to Infisical is in progress. The import process may take up to 30 + minutes. You will receive an email once the import has completed. + +
+
+ ); +}; + +export default ExternalImportStartedTemplate; + +ExternalImportStartedTemplate.PreviewProps = { + provider: "EnvKey" +} as ExternalImportStartedTemplateProps; diff --git a/backend/src/services/smtp/emails/ExternalImportSucceededTemplate.tsx b/backend/src/services/smtp/emails/ExternalImportSucceededTemplate.tsx new file mode 100644 index 000000000..390d50fbc --- /dev/null +++ b/backend/src/services/smtp/emails/ExternalImportSucceededTemplate.tsx @@ -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 { + provider: string; +} + +export const ExternalImportSucceededTemplate = ({ siteUrl, provider }: ExternalImportSucceededTemplateProps) => { + return ( + + + An import from {provider} to Infisical has completed + +
+ + An import from {provider} to Infisical was successful. Your data is now available in + Infisical. + +
+
+ ); +}; + +export default ExternalImportSucceededTemplate; + +ExternalImportSucceededTemplate.PreviewProps = { + provider: "EnvKey", + siteUrl: "https://infisical.com" +} as ExternalImportSucceededTemplateProps; diff --git a/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx b/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx new file mode 100644 index 000000000..9eeeb4c3f --- /dev/null +++ b/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx @@ -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 { + count: number; + projectName: string; + secretPath: string; + environment: string; + syncMessage: string; + integrationUrl: string; +} + +export const IntegrationSyncFailedTemplate = ({ + count, + siteUrl, + projectName, + secretPath, + environment, + syncMessage, + integrationUrl +}: IntegrationSyncFailedTemplateProps) => { + return ( + + + {count} integration(s) failed to sync + +
+ Project + {projectName} + Environment + {environment} + Secret Path + {secretPath} + Failure Reason: + "{syncMessage}" +
+
+ +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx b/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx new file mode 100644 index 000000000..3d301c0ab --- /dev/null +++ b/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx @@ -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 { + email: string; + timestamp: string; + ip: string; + userAgent: string; + isCloud: boolean; +} + +export const NewDeviceLoginTemplate = ({ + email, + timestamp, + ip, + userAgent, + siteUrl, + isCloud +}: NewDeviceLoginTemplateProps) => { + return ( + + + We're verifying a recent login for +
+ {email} +
+
+ Timestamp + {timestamp} + IP Address + {ip} + User Agent + {userAgent} +
+
+ + If you believe that this login is suspicious, please contact{" "} + {isCloud ? ( + + support@infisical.com + + ) : ( + "your administrator" + )}{" "} + or reset your password immediately. + +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx new file mode 100644 index 000000000..f3d0d01d7 --- /dev/null +++ b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx @@ -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 { + email: string; + timestamp: string; + ip: string; + userAgent: string; +} + +export const OrgAdminBreakglassAccessTemplate = ({ + email, + siteUrl, + timestamp, + ip, + userAgent +}: OrgAdminBreakglassAccessTemplateProps) => { + return ( + + + The organization admin {email} has bypassed enforced SSO login. + +
+ Timestamp + {timestamp} + IP Address + {ip} + User Agent + {userAgent} + + If you'd like to disable Admin SSO Bypass, please visit{" "} + + Organization Security Settings + + . + +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx b/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx new file mode 100644 index 000000000..4a9eeae3e --- /dev/null +++ b/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx @@ -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 { + email: string; + projectName: string; +} + +export const OrgAdminProjectGrantAccessTemplate = ({ + email, + siteUrl, + projectName +}: OrgAdminProjectGrantAccessTemplateProps) => { + return ( + + + An organization admin has joined the project {projectName} + +
+ + The organization admin {email} has self-issued direct access to the project{" "} + {projectName}. + +
+
+ ); +}; + +export default OrgAdminProjectGrantAccessTemplate; + +OrgAdminProjectGrantAccessTemplate.PreviewProps = { + email: "kevin@infisical.com", + projectName: "Example Project" +} as OrgAdminProjectGrantAccessTemplateProps; diff --git a/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx b/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx new file mode 100644 index 000000000..6e27d1e9b --- /dev/null +++ b/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx @@ -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 { + 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 ( + + + You've been invited to join +
+ {organizationName} on Infisical +
+
+ + {inviterFirstName} ( + + {inviterUsername} + + ) has invited you to collaborate on {organizationName}. + +
+
+ +
+
+ + About Infisical: Infisical is an all-in-one platform to securely manage application secrets, + certificates, SSH keys, and configurations across your team and infrastructure. + +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/PasswordResetTemplate.tsx b/backend/src/services/smtp/emails/PasswordResetTemplate.tsx new file mode 100644 index 000000000..b4ac0d5a7 --- /dev/null +++ b/backend/src/services/smtp/emails/PasswordResetTemplate.tsx @@ -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 { + email: string; + callback_url: string; + token: string; + isCloud: boolean; +} + +export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, token }: PasswordResetTemplateProps) => { + return ( + + + Account Recovery + +
+ A password reset was requested for your Infisical account. + + If you did not initiate this request, please contact{" "} + {isCloud ? ( + <> + us immediately at{" "} + + support@infisical.com + + + ) : ( + "your administrator immediately" + )} + . + +
+
+ +
+
+ ); +}; + +export default PasswordResetTemplate; + +PasswordResetTemplate.PreviewProps = { + email: "kevin@infisical.com", + callback_url: "https://app.infisical.com", + isCloud: true +} as PasswordResetTemplateProps; diff --git a/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx b/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx new file mode 100644 index 000000000..a03724a39 --- /dev/null +++ b/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx @@ -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 { + email: string; + callback_url: string; + token: string; + isCloud: boolean; +} + +export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, token }: PasswordSetupTemplateProps) => { + return ( + + + Password Setup + +
+ Someone requested to set up a password for your Infisical account. + + Make sure you are already logged in to Infisical in the current browser before clicking the link below. + + + If you did not initiate this request, please contact{" "} + {isCloud ? ( + <> + us immediately at{" "} + + support@infisical.com + + + ) : ( + "your administrator immediately" + )} + . + +
+
+ +
+
+ ); +}; + +export default PasswordSetupTemplate; + +PasswordSetupTemplate.PreviewProps = { + email: "casey@infisical.com", + callback_url: "https://app.infisical.com", + isCloud: true +} as PasswordSetupTemplateProps; diff --git a/backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx b/backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx new file mode 100644 index 000000000..db3b4816e --- /dev/null +++ b/backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx @@ -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 { + alertName: string; + alertBeforeDays: number; + items: { type: string; friendlyName: string; serialNumber: string; expiryDate: string }[]; +} + +export const PkiExpirationAlertTemplate = ({ + alertName, + siteUrl, + alertBeforeDays, + items +}: PkiExpirationAlertTemplateProps) => { + return ( + + + CA/Certificate Expiration Notice + +
+ Hello, + + This is an automated alert for {alertName} triggered for CAs/Certificates expiring in{" "} + {alertBeforeDays} days. + + + Expiring Items: + + {items.map((item) => ( + +
+ {item.type}: + {item.friendlyName} + Serial Number: + {item.serialNumber} + Expires On: + {item.expiryDate} +
+ ))} +
+ + Please take the necessary actions to renew these items before they expire. + + + For more details, please log in to your Infisical account and check your PKI management section. + +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx b/backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx new file mode 100644 index 000000000..28933b538 --- /dev/null +++ b/backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx @@ -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 { + 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 ( + + + A user has requested access to the project {projectName} + +
+ + {requesterName} ( + + {requesterEmail} + + ) has requested access to the project {projectName} in the organization{" "} + {orgName}. + + + User note: "{note}" + +
+
+ +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx b/backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx new file mode 100644 index 000000000..452b098a5 --- /dev/null +++ b/backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx @@ -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 { + callback_url: string; + workspaceName: string; +} + +export const ProjectInvitationTemplate = ({ callback_url, workspaceName, siteUrl }: ProjectInvitationTemplateProps) => { + return ( + + + You've been invited to join a new project on Infisical + +
+ + You've been invited to join the project {workspaceName}. + +
+
+ +
+
+ + About Infisical: Infisical is an all-in-one platform to securely manage application secrets, + certificates, SSH keys, and configurations across your team and infrastructure. + +
+
+ ); +}; + +export default ProjectInvitationTemplate; + +ProjectInvitationTemplate.PreviewProps = { + workspaceName: "Example Project", + siteUrl: "https://infisical.com", + callback_url: "https://app.infisical.com" +} as ProjectInvitationTemplateProps; diff --git a/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx b/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx new file mode 100644 index 000000000..722894835 --- /dev/null +++ b/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx @@ -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 { + organizationName: string; + callback_url: string; +} + +export const ScimUserProvisionedTemplate = ({ + organizationName, + callback_url, + siteUrl +}: ScimUserProvisionedTemplateProps) => { + return ( + + + You've been invited to join +
+ {organizationName} on Infisical +
+
+ + You've been invited you to collaborate on {organizationName}. + +
+
+ +
+
+ + About Infisical: Infisical is an all-in-one platform to securely manage application secrets, + certificates, SSH keys, and configurations across your team and infrastructure. + +
+
+ ); +}; + +export default ScimUserProvisionedTemplate; + +ScimUserProvisionedTemplate.PreviewProps = { + organizationName: "Example Organization", + callback_url: "https://app.infisical.com", + siteUrl: "https://app.infisical.com" +} as ScimUserProvisionedTemplateProps; diff --git a/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx b/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx new file mode 100644 index 000000000..bad823bd3 --- /dev/null +++ b/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx @@ -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 { + 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 ( + + + A secret approval request has been bypassed in the project {projectName} + +
+ + {requesterFullName} ( + + {requesterEmail} + + ) has merged a secret to {secretPath} in the {environment} environment + without obtaining the required approval. + + + The following reason was provided for bypassing the policy: " + {bypassReason}" + +
+
+ +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx b/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx new file mode 100644 index 000000000..faff34b58 --- /dev/null +++ b/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx @@ -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 { + projectName: string; + firstName: string; + organizationName: string; + approvalUrl: string; +} + +export const SecretApprovalRequestNeedsReviewTemplate = ({ + projectName, + siteUrl, + firstName, + organizationName, + approvalUrl +}: SecretApprovalRequestNeedsReviewTemplateProps) => { + return ( + + + A secret approval request for the project {projectName} requires review. + +
+ Hello {firstName}, + + You have a new secret change request pending your review for the project {projectName} in the + organization {organizationName}. + +
+
+ +
+
+ ); +}; + +export default SecretApprovalRequestNeedsReviewTemplate; + +SecretApprovalRequestNeedsReviewTemplate.PreviewProps = { + firstName: "Gordon", + organizationName: "Example Org", + siteUrl: "https://infisical.com", + approvalUrl: "https://infisical.com", + projectName: "Example Project" +} as SecretApprovalRequestNeedsReviewTemplateProps; diff --git a/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx new file mode 100644 index 000000000..3ef10eb4e --- /dev/null +++ b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx @@ -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 { + numberOfSecrets: number; + pusher_email: string; + pusher_name: string; +} + +export const SecretLeakIncidentTemplate = ({ + numberOfSecrets, + siteUrl, + pusher_name, + pusher_email +}: SecretLeakIncidentTemplateProps) => { + return ( + + + Infisical has uncovered {numberOfSecrets} secret(s) from a recent commit + +
+ + You are receiving this notification because one or more leaked secrets have been detected in a recent commit + {(pusher_email || pusher_name) && ( + <> + {" "} + pushed by {pusher_name ?? "Unknown Pusher"}{" "} + {pusher_email && ( + <> + ( + + {pusher_email} + + ) + + )} + + )} + . + + + 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. + + + If these are production secrets, please rotate them immediately. + + + Once you have taken action, be sure to update the status of the risk in the{" "} + + Infisical Dashboard + + . + +
+
+ +
+
+ ); +}; + +export default SecretLeakIncidentTemplate; + +SecretLeakIncidentTemplate.PreviewProps = { + pusher_name: "Jim", + pusher_email: "jim@infisical.com", + numberOfSecrets: 3, + siteUrl: "https://infisical.com" +} as SecretLeakIncidentTemplateProps; diff --git a/backend/src/services/smtp/emails/SecretReminderTemplate.tsx b/backend/src/services/smtp/emails/SecretReminderTemplate.tsx new file mode 100644 index 000000000..753246db5 --- /dev/null +++ b/backend/src/services/smtp/emails/SecretReminderTemplate.tsx @@ -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 { + projectName: string; + organizationName: string; + reminderNote?: string; +} + +export const SecretReminderTemplate = ({ + siteUrl, + reminderNote, + projectName, + organizationName +}: SecretReminderTemplateProps) => { + return ( + + + Secret Reminder + +
+ + You have a new secret reminder from the project {projectName} in the{" "} + {organizationName} organization. + + {reminderNote && ( + + Reminder Note: "{reminderNote}" + + )} +
+
+ ); +}; + +export default SecretReminderTemplate; + +SecretReminderTemplate.PreviewProps = { + reminderNote: "Remember to rotate secret.", + projectName: "Example Project", + organizationName: "Example Organization", + siteUrl: "https://infisical.com" +} as SecretReminderTemplateProps; diff --git a/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx b/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx new file mode 100644 index 000000000..75e2d1ba5 --- /dev/null +++ b/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx @@ -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 { + name?: string; + respondentUsername: string; + secretRequestUrl: string; +} + +export const SecretRequestCompletedTemplate = ({ + name, + siteUrl, + respondentUsername, + secretRequestUrl +}: ShareSecretTemplateProps) => { + return ( + + + A secret has been shared with you + +
+ + {respondentUsername ? {respondentUsername} : "Someone"} shared a secret{" "} + {name && ( + <> + {name}{" "} + + )}{" "} + with you. + +
+
+ +
+
+ ); +}; + +export default SecretRequestCompletedTemplate; + +SecretRequestCompletedTemplate.PreviewProps = { + respondentUsername: "Gracie", + siteUrl: "https://infisical.com", + secretRequestUrl: "https://infisical.com", + name: "API_TOKEN" +} as ShareSecretTemplateProps; diff --git a/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx b/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx new file mode 100644 index 000000000..6b149dcf3 --- /dev/null +++ b/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx @@ -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 { + 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 ( + + + Your {rotationType} Rotation {rotationName} failed to rotate + +
+ Name + {rotationName} + Type + {rotationType} + Project + {projectName} + Environment + {environment} + Secret Path + {secretPath} + Reason: + {content} +
+
+ +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx b/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx new file mode 100644 index 000000000..9cad01250 --- /dev/null +++ b/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx @@ -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 { + 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 ( + + + Your {syncDestination} Sync {syncName} failed to sync + +
+ Name + {syncName} + Destination + {syncDestination} + Project + {projectName} + {environment && ( + <> + Environment + {environment} + + )} + {secretPath && ( + <> + Secret Path + {secretPath} + + )} + {failureMessage && ( + <> + Reason: + {failureMessage} + + )} +
+
+ +
+
+ ); +}; + +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; diff --git a/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx b/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx new file mode 100644 index 000000000..a45b83d7d --- /dev/null +++ b/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx @@ -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 { + tokenName: string; + projectName: string; + url: string; +} + +export const ServiceTokenExpiryNoticeTemplate = ({ + tokenName, + siteUrl, + projectName, + url +}: ServiceTokenExpiryNoticeTemplateProps) => { + return ( + + + Service token expiry notice + +
+ + Your service token {tokenName} for the project {projectName} will expire + within 24 hours. + + If this token is still needed for your workflow, please create a new one before it expires. +
+
+ +
+
+ ); +}; + +export default ServiceTokenExpiryNoticeTemplate; + +ServiceTokenExpiryNoticeTemplate.PreviewProps = { + projectName: "Example Project", + siteUrl: "https://infisical.com", + url: "https://infisical.com", + tokenName: "Example Token" +} as ServiceTokenExpiryNoticeTemplateProps; diff --git a/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx b/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx new file mode 100644 index 000000000..66bb8e633 --- /dev/null +++ b/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx @@ -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 { + code: string; + isCloud: boolean; +} + +export const SignupEmailVerificationTemplate = ({ code, siteUrl, isCloud }: SignupEmailVerificationTemplateProps) => { + return ( + + + Confirm your email address + +
+ Enter the confirmation code below in the browser where you started sign-up. + + {code} + +
+
+ + Questions about setting up Infisical?{" "} + {isCloud ? ( + <> + Email us at{" "} + + support@infisical.com + + + ) : ( + "Contact your administrator" + )} + . + +
+
+ ); +}; + +export default SignupEmailVerificationTemplate; + +SignupEmailVerificationTemplate.PreviewProps = { + code: "124356", + isCloud: true, + siteUrl: "https://infisical.com" +} as SignupEmailVerificationTemplateProps; diff --git a/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx b/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx new file mode 100644 index 000000000..daeabf4fd --- /dev/null +++ b/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx @@ -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 { + token: string; + callback_url: string; +} + +export const UnlockAccountTemplate = ({ token, siteUrl, callback_url }: UnlockAccountTemplateProps) => { + return ( + + + Unlock your Infisical account + +
+ + Your account has been temporarily locked due to multiple failed login attempts. + + If these attempts were not made by you, reset your password immediately. +
+
+ +
+
+ ); +}; + +export default UnlockAccountTemplate; + +UnlockAccountTemplate.PreviewProps = { + callback_url: "Example Project", + siteUrl: "https://infisical.com", + url: "https://infisical.com", + token: "Example Token" +} as UnlockAccountTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts new file mode 100644 index 000000000..324f7063e --- /dev/null +++ b/backend/src/services/smtp/emails/index.ts @@ -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"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts deleted file mode 100644 index 550e1bb07..000000000 --- a/backend/src/services/smtp/smtp-service.ts +++ /dev/null @@ -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; - -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( - `

Email sent via Infisical at ${SITE_URL}

` - ); - }); - - 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 }; -}; diff --git a/backend/src/services/smtp/smtp-service.tsx b/backend/src/services/smtp/smtp-service.tsx new file mode 100644 index 000000000..dd1755aaf --- /dev/null +++ b/backend/src/services/smtp/smtp-service.tsx @@ -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; + +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.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( + `

Email sent via Infisical at ${SITE_URL}

` + ); + }); + + 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( + + ); + + 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 }; +}; diff --git a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars deleted file mode 100644 index 6813c1200..000000000 --- a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - Access Approval Request - - - -

Infisical

-

New access approval request pending your review

-

You have a new access approval request pending review in project "{{projectName}}".

- -

- {{requesterFullName}} - ({{requesterEmail}}) has requested - {{#if isTemporary}} - temporary - {{else}} - permanent - {{/if}} - access to - {{secretPath}} - in the - {{environment}} - environment. - - {{#if isTemporary}} -
- This access will expire - {{expiresIn}} - after it has been approved. - {{/if}} -

-

- The following permissions are requested: -

    - {{#each permissions}} -
  • {{this}}
  • - {{/each}} -
-

- {{#if note}} -

User Note: "{{note}}"

- {{/if}} - -

- View the request and approve or deny it - here. -

- - {{emailFooter}} - - - diff --git a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars deleted file mode 100644 index 8c82df289..000000000 --- a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars +++ /dev/null @@ -1,33 +0,0 @@ - - - - - Secret Approval Request Policy Bypassed - - - -

Infisical

-

Secret Approval Request Bypassed

-

A secret approval request has been bypassed in the project "{{projectName}}".

- -

- {{requesterFullName}} - ({{requesterEmail}}) has merged a secret to environment - {{environment}} - at secret path - {{secretPath}} - without obtaining the required approvals. -

-

- The following reason was provided for bypassing the policy: - {{bypassReason}} -

- -

- To review this action, please visit the request panel - here. -

- - {{emailFooter}} - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/emailMfa.handlebars b/backend/src/services/smtp/templates/emailMfa.handlebars deleted file mode 100644 index 4c948b08c..000000000 --- a/backend/src/services/smtp/templates/emailMfa.handlebars +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - MFA Code - - - -

Infisical

-

Sign in attempt requires further verification

-

Your MFA code is below — enter it where you started signing in to Infisical.

-

{{code}}

-

The MFA code will be valid for 2 minutes.

-

Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/emailVerification.handlebars b/backend/src/services/smtp/templates/emailVerification.handlebars deleted file mode 100644 index 4a989626e..000000000 --- a/backend/src/services/smtp/templates/emailVerification.handlebars +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Code - - - -

Confirm your email address

-

Your confirmation code is below — enter it in the browser window where you've started confirming your email.

-

{{code}}

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars b/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars deleted file mode 100644 index 4a918ee0d..000000000 --- a/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Incident alert: secrets potentially leaked - - - -

Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo

-

View leaked secrets

- -

If these are production secrets, please rotate them immediately.

- -

Once you have taken action, be sure to update the status of the risk in your - Infisical dashboard.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars deleted file mode 100644 index 2aff820fa..000000000 --- a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - Integration Sync Failed - - - -

Infisical

- -
-

{{count}} integration(s) failed to sync.

- - View your project integrations. - -
- -
-
-

Project: {{projectName}}

-

Environment: {{environment}}

-

Secret Path: {{secretPath}}

-
- - {{#if syncMessage}} -

Reason: {{syncMessage}}

- {{/if}} - - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/newDevice.handlebars b/backend/src/services/smtp/templates/newDevice.handlebars deleted file mode 100644 index 197e0b7a7..000000000 --- a/backend/src/services/smtp/templates/newDevice.handlebars +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - Successful login for {{email}} from new device - - - -

Infisical

-

We're verifying a recent login for {{email}}:

-

Timestamp: {{timestamp}}

-

IP address: {{ip}}

-

User agent: {{userAgent}}

-

If you believe that this login is suspicious, please contact - {{#if isCloud}}Infisical{{else}}your administrator{{/if}} - or reset your password immediately.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars b/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars deleted file mode 100644 index cc97ff201..000000000 --- a/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Organization admin has bypassed SSO - - - -

Infisical

-

The organization admin {{email}} has bypassed enforced SSO login.

-

Timestamp: {{timestamp}}

-

IP address: {{ip}}

-

User agent: {{userAgent}}

-

If you'd like to disable Admin SSO Bypass, please visit Organization Settings > Security.

- - {{emailFooter}} - - - diff --git a/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars b/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars deleted file mode 100644 index ef8c6e6b4..000000000 --- a/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Organization admin issued direct access to project - - - -

Infisical

-

The organization admin {{email}} has granted direct access to the project "{{projectName}}".

- - {{emailFooter}} - - - diff --git a/backend/src/services/smtp/templates/organizationInvitation.handlebars b/backend/src/services/smtp/templates/organizationInvitation.handlebars deleted file mode 100644 index da429477b..000000000 --- a/backend/src/services/smtp/templates/organizationInvitation.handlebars +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - Organization Invitation - - -

Join your organization on Infisical

-

{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization named {{organizationName}}

- Click to join -

What is Infisical?

-

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

- - {{emailFooter}} - - diff --git a/backend/src/services/smtp/templates/passwordReset.handlebars b/backend/src/services/smtp/templates/passwordReset.handlebars deleted file mode 100644 index 1cb2ae8ce..000000000 --- a/backend/src/services/smtp/templates/passwordReset.handlebars +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Account Recovery - - -

Reset your password

-

Someone requested a password reset.

- Reset password -

If you didn't initiate this request, please contact - {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

- - {{emailFooter}} - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/passwordSetup.handlebars b/backend/src/services/smtp/templates/passwordSetup.handlebars deleted file mode 100644 index 1059a7446..000000000 --- a/backend/src/services/smtp/templates/passwordSetup.handlebars +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Password Setup - - -

Setup your password

-

Someone requested to set up a password for your account.

-

Make sure you are already logged in to Infisical in the current browser before clicking the link below.

- Setup password -

If you didn't initiate this request, please contact - {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

- - {{emailFooter}} - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars b/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars deleted file mode 100644 index f9013e24d..000000000 --- a/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars +++ /dev/null @@ -1,33 +0,0 @@ - - - - - Infisical CA/Certificate expiration notice - - -

Hello,

-

This is an automated alert for "{{alertName}}" triggered for CAs/Certificates expiring in - {{alertBeforeDays}} - days.

- -

Expiring Items:

-
    - {{#each items}} -
  • - {{type}}: - {{friendlyName}} -
    Serial Number: - {{serialNumber}} -
    Expires On: - {{expiryDate}} -
  • - {{/each}} -
- -

Please take necessary actions to renew these items before they expire.

- -

For more details, please log in to your Infisical account and check your PKI management section.

- - {{emailFooter}} - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/projectAccess.handlebars b/backend/src/services/smtp/templates/projectAccess.handlebars deleted file mode 100644 index 5ff1ca7ec..000000000 --- a/backend/src/services/smtp/templates/projectAccess.handlebars +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - Project Access Request - - - -

Infisical

-

You have a new project access request!

-
    -
  • Requester Name: "{{requesterName}}"
  • -
  • Requester Email: "{{requesterEmail}}"
  • -
  • Project Name: "{{projectName}}"
  • -
  • Organization Name: "{{orgName}}"
  • -
  • User Note: "{{note}}"
  • -
-

- Please click on the link below to grant access -

- Grant Access - {{emailFooter}} - - - diff --git a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars deleted file mode 100644 index ba04d7201..000000000 --- a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Organization Invitation - - -

Join your organization on Infisical

-

You've been invited to join the Infisical organization — {{organizationName}}

- Join now -

What is Infisical?

-

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets - and configs.

- - {{emailFooter}} - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars b/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars deleted file mode 100644 index c12c08460..000000000 --- a/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Secret Change Approval Request - - - -

Hi {{firstName}},

-

New secret change requests are pending review.

-
-

You have a secret change request pending your review in project "{{projectName}}", in the "{{organizationName}}" - organization.

- -

- View the request and approve or deny it - here. -

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretLeakIncident.handlebars b/backend/src/services/smtp/templates/secretLeakIncident.handlebars deleted file mode 100644 index d0d9a617c..000000000 --- a/backend/src/services/smtp/templates/secretLeakIncident.handlebars +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - Incident alert: secret leaked - - - -

Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push

-

View leaked secrets

-

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).

- -

If these are production secrets, please rotate them immediately.

- -

Once you have taken action, be sure to update the status of the risk in your - Infisical dashboard.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretReminder.handlebars b/backend/src/services/smtp/templates/secretReminder.handlebars deleted file mode 100644 index d64c4bf42..000000000 --- a/backend/src/services/smtp/templates/secretReminder.handlebars +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Secret Reminder - - - -

Infisical

-

You have a new secret reminder!

-

You have a new secret reminder from project "{{projectName}}", in {{organizationName}}

- {{#if reminderNote}} -

Here's the note included with the reminder: {{reminderNote}}

- {{/if}} - - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretRequestCompleted.handlebars b/backend/src/services/smtp/templates/secretRequestCompleted.handlebars deleted file mode 100644 index d2cefdd54..000000000 --- a/backend/src/services/smtp/templates/secretRequestCompleted.handlebars +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - Secret Request Completed - - - -

Infisical

-

A secret has been shared with you

- - {{#if name}} -

Secret request name: {{name}}

- {{/if}} - {{#if respondentUsername}} -

Shared by: {{respondentUsername}}

- {{/if}} - -
-
- -

- You can access the secret by clicking the link below. -

-

- Access Secret -

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretRotationFailed.handlebars b/backend/src/services/smtp/templates/secretRotationFailed.handlebars deleted file mode 100644 index 728798ce8..000000000 --- a/backend/src/services/smtp/templates/secretRotationFailed.handlebars +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Your {{rotationType}} Rotation "{{rotationName}}" Failed to Rotate - - - -

Infisical

- - - -
-
-

Name: {{rotationName}}

-

Type: {{rotationType}}

-

Project: {{projectName}}

-

Environment: {{environment}}

-

Secret Path: {{secretPath}}

-
- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretSyncFailed.handlebars b/backend/src/services/smtp/templates/secretSyncFailed.handlebars deleted file mode 100644 index 3e7ad7831..000000000 --- a/backend/src/services/smtp/templates/secretSyncFailed.handlebars +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - {{syncDestination}} Sync "{{syncName}}" Failed - - - -

Infisical

- -
-

{{content}}

- - View in Infisical. - -
- -
-
-

Name: {{syncName}}

-

Destination: {{syncDestination}}

-

Project: {{projectName}}

- {{#if environment}} -

Environment: {{environment}}

- {{/if}} - {{#if secretPath}} -

Secret Path: {{secretPath}}

- {{/if}} -
- - {{#if failureMessage}} -

Reason: {{failureMessage}}

- {{/if}} - - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars deleted file mode 100644 index 199150c05..000000000 --- a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Service Token Expiring Soon - - - -

Service Token Expiry Notice

-

Your service token "{{tokenName}}" will expire within 24 hours.

- -

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.

- - Create New Token - - {{emailFooter}} - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/signupEmailVerification.handlebars b/backend/src/services/smtp/templates/signupEmailVerification.handlebars deleted file mode 100644 index 39f47ae48..000000000 --- a/backend/src/services/smtp/templates/signupEmailVerification.handlebars +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Code - - - -

Confirm your email address

-

Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.

-

{{code}}

-

Questions about setting up Infisical? - {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/unlockAccount.handlebars b/backend/src/services/smtp/templates/unlockAccount.handlebars deleted file mode 100644 index b65cb5625..000000000 --- a/backend/src/services/smtp/templates/unlockAccount.handlebars +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Your Infisical account has been locked - - - -

Unlock your Infisical account

-

Your account has been temporarily locked due to multiple failed login attempts. - To unlock your account, follow the link here -

If these attempts were not made by you, reset your password immediately.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/workspaceInvitation.handlebars b/backend/src/services/smtp/templates/workspaceInvitation.handlebars deleted file mode 100644 index fde75a6d6..000000000 --- a/backend/src/services/smtp/templates/workspaceInvitation.handlebars +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Project Invitation - - -

Join your team on Infisical

-

You have been invited to a new Infisical project named {{workspaceName}}

- Click to join -

What is Infisical?

-

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets - and configs.

- - {{emailFooter}} - - diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8c23a00c6..8687826c8 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -172,8 +172,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 || diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 90165acbe..3d7813257 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -25,7 +25,8 @@ "baseUrl": ".", "paths": { "@app/*": ["./src/*"] - } + }, + "jsx": "react" }, "include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"], "exclude": ["node_modules"] From a80d5f10e5d5833845881dadb312e92936b7c673 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 2 May 2025 16:38:02 +0400 Subject: [PATCH 12/33] fix(gateway-helm): requested changes --- .github/workflows/release_docker_gateway.yaml | 43 ------------------- .../templates/deployment.yaml | 10 ++++- .../infisical-gateway/templates/secret.yaml | 13 ------ helm-charts/infisical-gateway/values.yaml | 15 ++----- 4 files changed, 13 insertions(+), 68 deletions(-) delete mode 100644 .github/workflows/release_docker_gateway.yaml delete mode 100644 helm-charts/infisical-gateway/templates/secret.yaml diff --git a/.github/workflows/release_docker_gateway.yaml b/.github/workflows/release_docker_gateway.yaml deleted file mode 100644 index da1e105e3..000000000 --- a/.github/workflows/release_docker_gateway.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Release Gateway Docker Image -on: - push: - tags: - - "infisical-gateway/v*.*.*" - -permissions: - contents: write - -jobs: - release-image: - name: Release Gateway Docker Image - runs-on: ubuntu-latest - steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-gateway/}" - - - name: Checkout code - uses: actions/checkout@v4 - - - name: 🔧 Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: | - infisical/gateway:latest - infisical/gateway:${{ steps.extract_version.outputs.version }} diff --git a/helm-charts/infisical-gateway/templates/deployment.yaml b/helm-charts/infisical-gateway/templates/deployment.yaml index 636e88d35..2671c0665 100644 --- a/helm-charts/infisical-gateway/templates/deployment.yaml +++ b/helm-charts/infisical-gateway/templates/deployment.yaml @@ -35,8 +35,16 @@ spec: securityContext: {{- toYaml . | nindent 12 }} {{- end }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + image: "infisical/cli:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/sbin/tini", "--", "/bin/sh", "-c"] + args: + - >- + if [ -z "${TOKEN}" ]; then + echo 'ERROR: TOKEN environment variable must be set'; + exit 1; + fi && + /bin/infisical gateway --token ${TOKEN} envFrom: - secretRef: name: {{ .Values.secret.name }} diff --git a/helm-charts/infisical-gateway/templates/secret.yaml b/helm-charts/infisical-gateway/templates/secret.yaml deleted file mode 100644 index f186b8ec4..000000000 --- a/helm-charts/infisical-gateway/templates/secret.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.secret.create -}} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.secret.name }} - labels: - {{- include "infisical-gateway.labels" . | nindent 4 }} -type: Opaque -data: - {{- range $key, $value := .Values.secret.data }} - {{ $key }}: {{ $value | b64enc }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/helm-charts/infisical-gateway/values.yaml b/helm-charts/infisical-gateway/values.yaml index bb63a82a8..b1d55c0fc 100644 --- a/helm-charts/infisical-gateway/values.yaml +++ b/helm-charts/infisical-gateway/values.yaml @@ -1,18 +1,10 @@ image: - repository: infisical/infisical-gateway pullPolicy: IfNotPresent - tag: "v0.0.1" + tag: "0.41.1" secret: - # create a new secret (if true) or use existing (if false) - create: true - # name of the secret to use/create - name: "infisical-gateway-secrets" - - # Secret data (only used if create: true) - data: - TOKEN: "" - INFISICAL_API_URL: "https://app.infisical.com" + # 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: @@ -39,6 +31,7 @@ podSecurityContext: securityContext: runAsNonRoot: true + runAsUser: 1000 affinity: {} tolerations: {} From 5dbded60f4c07981320de600da8b2c8887ebb46b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 2 May 2025 16:38:31 +0400 Subject: [PATCH 13/33] Delete Dockerfile.gateway --- Dockerfile.gateway | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 Dockerfile.gateway diff --git a/Dockerfile.gateway b/Dockerfile.gateway deleted file mode 100644 index 24c03e3e0..000000000 --- a/Dockerfile.gateway +++ /dev/null @@ -1,24 +0,0 @@ -# Build stage -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY ./cli . - -RUN go build -o infisical-cli . - -FROM alpine:3.19 - -ENV TOKEN="" -ENV INFISICAL_API_URL="https://app.infisical.com" - - - -WORKDIR /app - -COPY --from=builder /app/infisical-cli . - -# Make the binary executable -RUN chmod +x /app/infisical-cli - -CMD ["sh", "-c", "if [ -z \"${TOKEN}\" ]; then echo 'ERROR: TOKEN environment variable must be set'; exit 1; fi && ./infisical-cli gateway --token ${TOKEN} --domain ${INFISICAL_API_URL}"] \ No newline at end of file From c7e707f20a86f00cd54516a437f91f3bccec3fe0 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 2 May 2025 11:08:41 -0700 Subject: [PATCH 14/33] improvement: address feedback --- backend/src/@types/fastify.d.ts | 2 +- backend/src/server/routes/v1/index.ts | 2 +- .../services/smtp/emails/EmailMfaTemplate.tsx | 4 ++-- .../smtp/emails/EmailVerificationTemplate.tsx | 6 ++--- .../emails/ExternalImportFailedTemplate.tsx | 2 +- .../emails/ExternalImportStartedTemplate.tsx | 3 ++- .../emails/IntegrationSyncFailedTemplate.tsx | 2 +- .../smtp/emails/NewDeviceLoginTemplate.tsx | 2 +- .../OrgAdminBreakglassAccessTemplate.tsx | 4 ++-- .../OrgAdminProjectGrantAccessTemplate.tsx | 3 ++- .../emails/OrganizationInvitationTemplate.tsx | 8 ++++--- .../smtp/emails/PasswordResetTemplate.tsx | 10 +++++---- .../smtp/emails/PasswordSetupTemplate.tsx | 10 +++++---- .../emails/PkiExpirationAlertTemplate.tsx | 11 +++++----- .../emails/ProjectAccessRequestTemplate.tsx | 4 ++-- .../smtp/emails/ProjectInvitationTemplate.tsx | 4 ++-- .../emails/ScimUserProvisionedTemplate.tsx | 4 ++-- ...cretApprovalRequestNeedsReviewTemplate.tsx | 4 ++-- .../emails/SecretLeakIncidentTemplate.tsx | 4 ++-- .../smtp/emails/SecretReminderTemplate.tsx | 2 +- .../emails/SecretRequestCompletedTemplate.tsx | 8 +++---- .../emails/SecretRotationFailedTemplate.tsx | 4 ++-- .../smtp/emails/SecretSyncFailedTemplate.tsx | 4 ++-- .../ServiceTokenExpiryNoticeTemplate.tsx | 2 +- .../SignupEmailVerificationTemplate.tsx | 4 ++-- .../smtp/emails/UnlockAccountTemplate.tsx | 5 ++--- backend/src/services/smtp/emails/index.ts | 1 - backend/src/services/smtp/smtp-service.tsx | 10 +-------- .../templates/externalImportFailed.handlebars | 22 ------------------- .../externalImportStarted.handlebars | 19 ---------------- .../externalImportSuccessful.handlebars | 16 -------------- backend/tsconfig.json | 2 +- 32 files changed, 65 insertions(+), 123 deletions(-) delete mode 100644 backend/src/services/smtp/templates/externalImportFailed.handlebars delete mode 100644 backend/src/services/smtp/templates/externalImportStarted.handlebars delete mode 100644 backend/src/services/smtp/templates/externalImportSuccessful.handlebars diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 92c22874e..77db3e59e 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -71,6 +71,7 @@ import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-a import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; @@ -100,7 +101,6 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; -import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; declare module "@fastify/request-context" { interface RequestContextData { diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index d2ba35a7e..a50299555 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -26,6 +26,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationRouter } from "./integration-router"; import { registerInviteOrgRouter } from "./invite-org-router"; +import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router"; import { registerOrgAdminRouter } from "./org-admin-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; @@ -47,7 +48,6 @@ import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; -import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); diff --git a/backend/src/services/smtp/emails/EmailMfaTemplate.tsx b/backend/src/services/smtp/emails/EmailMfaTemplate.tsx index 8a35b011d..b01e2f8af 100644 --- a/backend/src/services/smtp/emails/EmailMfaTemplate.tsx +++ b/backend/src/services/smtp/emails/EmailMfaTemplate.tsx @@ -15,7 +15,7 @@ export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplatePro MFA required
- Enter the MFA code below in the browser where you started sign-in. + Enter the MFA code shown below in the browser where you started sign-in. {code} @@ -26,7 +26,7 @@ export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplatePro {isCloud ? ( <> Contact us at{" "} - + support@infisical.com {" "} immediately diff --git a/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx b/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx index 6c0f410b1..fc32b01b0 100644 --- a/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx +++ b/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx @@ -12,14 +12,14 @@ export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerif return ( Confirm your email address
- Enter the confirmation code below in the browser window requiring confirmation. + Enter the confirmation code shown below in the browser window requiring confirmation. {code} @@ -30,7 +30,7 @@ export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerif {isCloud ? ( <> Email us at{" "} - + support@infisical.com diff --git a/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx b/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx index c283fdad9..3cca41721 100644 --- a/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx @@ -21,7 +21,7 @@ export const ExternalImportFailedTemplate = ({ error, siteUrl, provider }: Exter If your issue persists, you can contact the Infisical team at{" "} - + support@infisical.com . diff --git a/backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx b/backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx index fc24536b4..9b8ce6cf7 100644 --- a/backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx +++ b/backend/src/services/smtp/emails/ExternalImportStartedTemplate.tsx @@ -26,5 +26,6 @@ export const ExternalImportStartedTemplate = ({ siteUrl, provider }: ExternalImp export default ExternalImportStartedTemplate; ExternalImportStartedTemplate.PreviewProps = { - provider: "EnvKey" + provider: "EnvKey", + siteUrl: "https://infisical.com" } as ExternalImportStartedTemplateProps; diff --git a/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx b/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx index 9eeeb4c3f..c2fb78ae0 100644 --- a/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx @@ -24,7 +24,7 @@ export const IntegrationSyncFailedTemplate = ({ return ( diff --git a/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx b/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx index 3d301c0ab..9692bc77d 100644 --- a/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx +++ b/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx @@ -42,7 +42,7 @@ export const NewDeviceLoginTemplate = ({ If you believe that this login is suspicious, please contact{" "} {isCloud ? ( - + support@infisical.com ) : ( diff --git a/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx index f3d0d01d7..1d2ecc1a3 100644 --- a/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx +++ b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx @@ -24,7 +24,7 @@ export const OrgAdminBreakglassAccessTemplate = ({ siteUrl={siteUrl} > - The organization admin {email} has bypassed enforced SSO login. + The organization admin {email} has bypassed enforced SSO login
Timestamp @@ -53,5 +53,5 @@ OrgAdminBreakglassAccessTemplate.PreviewProps = { "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" + email: "august@infisical.com" } as OrgAdminBreakglassAccessTemplateProps; diff --git a/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx b/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx index 4a9eeae3e..bec22575c 100644 --- a/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx +++ b/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx @@ -36,5 +36,6 @@ export default OrgAdminProjectGrantAccessTemplate; OrgAdminProjectGrantAccessTemplate.PreviewProps = { email: "kevin@infisical.com", - projectName: "Example Project" + projectName: "Example Project", + siteUrl: "https://infisical.com" } as OrgAdminProjectGrantAccessTemplateProps; diff --git a/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx b/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx index 6e27d1e9b..b281e75d6 100644 --- a/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx +++ b/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx @@ -47,8 +47,8 @@ export const OrganizationInvitationTemplate = ({
@@ -71,5 +71,7 @@ OrganizationInvitationTemplate.PreviewProps = { inviterUsername: "jane@infisical.com", email: "john@infisical.com", siteUrl: "https://infisical.com", - callback_url: "https://app.infisical.com" + callback_url: "https://app.infisical.com", + token: "preview-token", + organizationId: "1ae1c2c7-8068-461c-b15e-421737868a6a" } as OrganizationInvitationTemplateProps; diff --git a/backend/src/services/smtp/emails/PasswordResetTemplate.tsx b/backend/src/services/smtp/emails/PasswordResetTemplate.tsx index b4ac0d5a7..7486b29d9 100644 --- a/backend/src/services/smtp/emails/PasswordResetTemplate.tsx +++ b/backend/src/services/smtp/emails/PasswordResetTemplate.tsx @@ -27,7 +27,7 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t {isCloud ? ( <> us immediately at{" "} - + support@infisical.com @@ -39,8 +39,8 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t
@@ -54,5 +54,7 @@ export default PasswordResetTemplate; PasswordResetTemplate.PreviewProps = { email: "kevin@infisical.com", callback_url: "https://app.infisical.com", - isCloud: true + isCloud: true, + token: "preview-token", + siteUrl: "https://infisical.com" } as PasswordResetTemplateProps; diff --git a/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx b/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx index a03724a39..c8a986c8c 100644 --- a/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx +++ b/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx @@ -26,7 +26,7 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t {isCloud ? ( <> us immediately at{" "} - + support@infisical.com @@ -38,8 +38,8 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t
@@ -53,5 +53,7 @@ export default PasswordSetupTemplate; PasswordSetupTemplate.PreviewProps = { email: "casey@infisical.com", callback_url: "https://app.infisical.com", - isCloud: true + isCloud: true, + siteUrl: "https://infisical.com", + token: "preview-token" } as PasswordSetupTemplateProps; diff --git a/backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx b/backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx index db3b4816e..a09a125a2 100644 --- a/backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx +++ b/backend/src/services/smtp/emails/PkiExpirationAlertTemplate.tsx @@ -45,10 +45,10 @@ export const PkiExpirationAlertTemplate = ({ ))}
- + Please take the necessary actions to renew these items before they expire. - + For more details, please log in to your Infisical account and check your PKI management section.
@@ -61,8 +61,9 @@ 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" } + { 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" + alertName: "My PKI Alert", + siteUrl: "https://infisical.com" } as PkiExpirationAlertTemplateProps; diff --git a/backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx b/backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx index 28933b538..afc32a86b 100644 --- a/backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx +++ b/backend/src/services/smtp/emails/ProjectAccessRequestTemplate.tsx @@ -8,7 +8,6 @@ interface ProjectAccessRequestTemplateProps extends Omit - You've been invited to join a new project on Infisical + You've been invited to join a project on Infisical
@@ -26,7 +26,7 @@ export const ProjectInvitationTemplate = ({ callback_url, workspaceName, siteUrl
diff --git a/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx b/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx index 722894835..bbd41818e 100644 --- a/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx +++ b/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx @@ -26,13 +26,13 @@ export const ScimUserProvisionedTemplate = ({
- You've been invited you to collaborate on {organizationName}. + You've been invited to collaborate on {organizationName}.
diff --git a/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx b/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx index faff34b58..b4a7c306a 100644 --- a/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx @@ -25,10 +25,10 @@ export const SecretApprovalRequestNeedsReviewTemplate = ({ siteUrl={siteUrl} > - A secret approval request for the project {projectName} requires review. + A secret approval request for the project {projectName} requires review
- Hello {firstName}, + Hello {firstName}, You have a new secret change request pending your review for the project {projectName} in the organization {organizationName}. diff --git a/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx index 3ef10eb4e..386566cc4 100644 --- a/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx @@ -62,8 +62,8 @@ export const SecretLeakIncidentTemplate = ({
diff --git a/backend/src/services/smtp/emails/SecretReminderTemplate.tsx b/backend/src/services/smtp/emails/SecretReminderTemplate.tsx index 753246db5..01b2e9044 100644 --- a/backend/src/services/smtp/emails/SecretReminderTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretReminderTemplate.tsx @@ -18,7 +18,7 @@ export const SecretReminderTemplate = ({ return ( - Secret Reminder + Secret Reminder
diff --git a/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx b/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx index 75e2d1ba5..4adeec636 100644 --- a/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx @@ -3,7 +3,7 @@ import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; -interface ShareSecretTemplateProps extends Omit { +interface SecretRequestCompletedTemplateProps extends Omit { name?: string; respondentUsername: string; secretRequestUrl: string; @@ -14,14 +14,14 @@ export const SecretRequestCompletedTemplate = ({ siteUrl, respondentUsername, secretRequestUrl -}: ShareSecretTemplateProps) => { +}: SecretRequestCompletedTemplateProps) => { return ( A secret has been shared with you
- + {respondentUsername ? {respondentUsername} : "Someone"} shared a secret{" "} {name && ( <> @@ -50,4 +50,4 @@ SecretRequestCompletedTemplate.PreviewProps = { siteUrl: "https://infisical.com", secretRequestUrl: "https://infisical.com", name: "API_TOKEN" -} as ShareSecretTemplateProps; +} as SecretRequestCompletedTemplateProps; diff --git a/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx b/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx index 6b149dcf3..52e58986d 100644 --- a/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx @@ -26,7 +26,7 @@ export const SecretRotationFailedTemplate = ({ return ( - Your {rotationType} Rotation {rotationName} failed to rotate + Your {rotationType} rotation {rotationName} failed to rotate
Name @@ -47,7 +47,7 @@ export const SecretRotationFailedTemplate = ({ 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 + View in Infisical
diff --git a/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx b/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx index 9cad01250..01d909219 100644 --- a/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx @@ -26,7 +26,7 @@ export const SecretSyncFailedTemplate = ({ return ( - Your {syncDestination} Sync {syncName} failed to sync + Your {syncDestination} sync {syncName} failed to complete
Name @@ -59,7 +59,7 @@ export const SecretSyncFailedTemplate = ({ 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 + View in Infisical
diff --git a/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx b/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx index a45b83d7d..8f8deb63a 100644 --- a/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx +++ b/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx @@ -25,7 +25,7 @@ export const ServiceTokenExpiryNoticeTemplate = ({ Service token expiry notice
- + Your service token {tokenName} for the project {projectName} will expire within 24 hours. diff --git a/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx b/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx index 66bb8e633..0a3da75f9 100644 --- a/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx +++ b/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx @@ -19,7 +19,7 @@ export const SignupEmailVerificationTemplate = ({ code, siteUrl, isCloud }: Sign Confirm your email address
- Enter the confirmation code below in the browser where you started sign-up. + Enter the confirmation code shown below in the browser where you started sign-up. {code} @@ -30,7 +30,7 @@ export const SignupEmailVerificationTemplate = ({ code, siteUrl, isCloud }: Sign {isCloud ? ( <> Email us at{" "} - + support@infisical.com diff --git a/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx b/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx index daeabf4fd..b0b4f2086 100644 --- a/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx +++ b/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx @@ -19,7 +19,7 @@ export const UnlockAccountTemplate = ({ token, siteUrl, callback_url }: UnlockAc Unlock your Infisical account
- + Your account has been temporarily locked due to multiple failed login attempts. If these attempts were not made by you, reset your password immediately. @@ -41,6 +41,5 @@ export default UnlockAccountTemplate; UnlockAccountTemplate.PreviewProps = { callback_url: "Example Project", siteUrl: "https://infisical.com", - url: "https://infisical.com", - token: "Example Token" + token: "preview-token" } as UnlockAccountTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 324f7063e..29738dbb2 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -9,7 +9,6 @@ export * from "./NewDeviceLoginTemplate"; export * from "./OrgAdminBreakglassAccessTemplate"; export * from "./OrgAdminProjectGrantAccessTemplate"; export * from "./OrganizationInvitationTemplate"; -export * from "./OrganizationInvitationTemplate"; export * from "./PasswordResetTemplate"; export * from "./PasswordSetupTemplate"; export * from "./PkiExpirationAlertTemplate"; diff --git a/backend/src/services/smtp/smtp-service.tsx b/backend/src/services/smtp/smtp-service.tsx index dd1755aaf..d3ab3ecda 100644 --- a/backend/src/services/smtp/smtp-service.tsx +++ b/backend/src/services/smtp/smtp-service.tsx @@ -1,5 +1,4 @@ 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"; @@ -80,7 +79,7 @@ export enum SmtpTemplates { 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" @@ -121,13 +120,6 @@ 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( - `

Email sent via Infisical at ${SITE_URL}

` - ); - }); - const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => { const appCfg = getConfig(); diff --git a/backend/src/services/smtp/templates/externalImportFailed.handlebars b/backend/src/services/smtp/templates/externalImportFailed.handlebars deleted file mode 100644 index 1755052c1..000000000 --- a/backend/src/services/smtp/templates/externalImportFailed.handlebars +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - Import failed - - - -

An import from {{provider}} to Infisical has failed

-

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

- -

Error: {{error}}

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportStarted.handlebars b/backend/src/services/smtp/templates/externalImportStarted.handlebars deleted file mode 100644 index 90026f762..000000000 --- a/backend/src/services/smtp/templates/externalImportStarted.handlebars +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Import in progress - - - -

An import from {{provider}} to Infisical is in progress

-

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.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportSuccessful.handlebars b/backend/src/services/smtp/templates/externalImportSuccessful.handlebars deleted file mode 100644 index a918e9ec7..000000000 --- a/backend/src/services/smtp/templates/externalImportSuccessful.handlebars +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Import successful - - - -

An import from {{provider}} to Infisical was successful

-

An import from {{provider}} was successful. Your data is now available in Infisical.

- - {{emailFooter}} - - - \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 3d7813257..523e6de5b 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -26,7 +26,7 @@ "paths": { "@app/*": ["./src/*"] }, - "jsx": "react" + "jsx": "react-jsx" }, "include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"], "exclude": ["node_modules"] From 8ad334b3ab1aa28bc5581b1afab0b8868711dd3c Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 2 May 2025 11:34:18 -0700 Subject: [PATCH 15/33] fix: try reverting ts jsx type --- backend/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 523e6de5b..3d7813257 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -26,7 +26,7 @@ "paths": { "@app/*": ["./src/*"] }, - "jsx": "react-jsx" + "jsx": "react" }, "include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"], "exclude": ["node_modules"] From 717c947e53ff095de337cd94dcb59eb92259688f Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 2 May 2025 11:42:20 -0700 Subject: [PATCH 16/33] fix: try removing jsx usage --- .../src/services/smtp/{smtp-service.tsx => smtp-service.ts} | 6 +++++- backend/tsconfig.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) rename backend/src/services/smtp/{smtp-service.tsx => smtp-service.ts} (97%) diff --git a/backend/src/services/smtp/smtp-service.tsx b/backend/src/services/smtp/smtp-service.ts similarity index 97% rename from backend/src/services/smtp/smtp-service.tsx rename to backend/src/services/smtp/smtp-service.ts index d3ab3ecda..dfb8320a3 100644 --- a/backend/src/services/smtp/smtp-service.tsx +++ b/backend/src/services/smtp/smtp-service.ts @@ -130,7 +130,11 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => { } const htmlToSend = await render( - + React.createElement(EmailTemplate, { + ...substitutions, + isCloud: appCfg.isCloud, + siteUrl: appCfg.SITE_URL + }) ); if (isSmtpOn) { diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 3d7813257..523e6de5b 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -26,7 +26,7 @@ "paths": { "@app/*": ["./src/*"] }, - "jsx": "react" + "jsx": "react-jsx" }, "include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"], "exclude": ["node_modules"] From 8f7a652741f40938691cecb4ad19665c5f8eb9fd Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 2 May 2025 11:57:18 -0700 Subject: [PATCH 17/33] fix: correct imports --- backend/src/services/smtp/smtp-service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index dfb8320a3..12b38ebb2 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -2,6 +2,10 @@ 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, @@ -12,6 +16,7 @@ import { IntegrationSyncFailedTemplate, NewDeviceLoginTemplate, OrgAdminBreakglassAccessTemplate, + OrgAdminProjectGrantAccessTemplate, OrganizationInvitationTemplate, PasswordResetTemplate, PasswordSetupTemplate, @@ -29,12 +34,7 @@ import { 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"; +} from "./emails"; export type TSmtpConfig = SMTPTransport.Options; export type TSmtpSendMail = { From 3ef053f255fbb7aa263fa0aebff914c5e4379b1f Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 2 May 2025 12:13:23 -0700 Subject: [PATCH 18/33] fix: test adding explicity .tsx path --- backend/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 523e6de5b..dba51e639 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -24,7 +24,8 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@app/*": ["./src/*"] + "@app/*": ["./src/*"], + "*": ["*", "*.tsx", "*.ts"] }, "jsx": "react-jsx" }, From f9f098af86df2c2a8684c3ee3702bfb25ee121a8 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 2 May 2025 12:20:17 -0700 Subject: [PATCH 19/33] fix: try updating tsup.config to account for .tsx --- backend/tsconfig.json | 3 +-- backend/tsup.config.js | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index dba51e639..523e6de5b 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -24,8 +24,7 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@app/*": ["./src/*"], - "*": ["*", "*.tsx", "*.ts"] + "@app/*": ["./src/*"] }, "jsx": "react-jsx" }, diff --git a/backend/tsup.config.js b/backend/tsup.config.js index 9e37870ba..e09a21ff2 100644 --- a/backend/tsup.config.js +++ b/backend/tsup.config.js @@ -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`, From dcb7215b7d58d6b8c72e4fb7c227f032342baede Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 3 May 2025 00:20:25 +0400 Subject: [PATCH 20/33] requested changes --- .../infisical-gateway/templates/deployment.yaml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/helm-charts/infisical-gateway/templates/deployment.yaml b/helm-charts/infisical-gateway/templates/deployment.yaml index 2671c0665..223bdabd8 100644 --- a/helm-charts/infisical-gateway/templates/deployment.yaml +++ b/helm-charts/infisical-gateway/templates/deployment.yaml @@ -37,17 +37,20 @@ spec: {{- end }} image: "infisical/cli:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - command: ["/sbin/tini", "--", "/bin/sh", "-c"] args: - - >- - if [ -z "${TOKEN}" ]; then - echo 'ERROR: TOKEN environment variable must be set'; - exit 1; - fi && - /bin/infisical gateway --token ${TOKEN} + - 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 }} From bec3cec040d189eee24e3ecf67d5ddc276bca023 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 2 May 2025 15:52:13 -0700 Subject: [PATCH 21/33] fix: correct secret-scanning link --- backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx index 386566cc4..631013756 100644 --- a/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx @@ -54,7 +54,7 @@ export const SecretLeakIncidentTemplate = ({
Once you have taken action, be sure to update the status of the risk in the{" "} - + Infisical Dashboard . From f5e34ea59ed4375d1276676d28be322008ccc4b9 Mon Sep 17 00:00:00 2001 From: Sheen <65645666+sheensantoscapadngan@users.noreply.github.com> Date: Sun, 4 May 2025 15:28:12 +0000 Subject: [PATCH 22/33] doc: added daemonset and statefulset auto-redeploy example --- .../kubernetes/infisical-secret-crd.mdx | 80 ++++++++++++++++++- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index 4c33b893b..a79a8d0a5 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -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" ``` - + ```yaml apiVersion: apps/v1 kind: Deployment @@ -1266,10 +1266,82 @@ secrets.infisical.com/auto-reload: "true" - containerPort: 80 ``` + + + ```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 + ``` + + + + ```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 + ``` + + #### 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. ## Using Managed ConfigMap In Your Deployment From c816cbc9a997f1b310e4a79db2ce30cce637db25 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 5 May 2025 00:09:59 +0400 Subject: [PATCH 23/33] docs(gateway-helm): helm deployment --- .../platform/gateways/overview.mdx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index 02d9c863a..93d5ddfd5 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -73,6 +73,61 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t + + + The Gateway can be install 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= + ``` + + + The secret name is `infisical-gateway-environment` by default. The `TOKEN` key is required, and the `INFISICAL_API_URL` key is optional. + + + ### 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 + ``` + + + For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command: ```bash From edd415aed8b52bf7653a2c9c8b24a9790c5f8172 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 5 May 2025 00:40:49 +0400 Subject: [PATCH 24/33] Update overview.mdx --- docs/documentation/platform/gateways/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index 93d5ddfd5..7ccc098cd 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -75,7 +75,7 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t - The Gateway can be install via [Helm](https://helm.sh/). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications. + 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: From 1ce06891a562f677c6bf8a909c05fa9c1d977ef1 Mon Sep 17 00:00:00 2001 From: Andrey Lyubavin Date: Mon, 5 May 2025 13:43:38 -0400 Subject: [PATCH 25/33] ui tweak for role policies --- .../components/GeneralPermissionPolicies.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index f0388a2f4..6773f0658 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -114,7 +114,7 @@ export const GeneralPermissionPolicies = )}
-
+
Actions
{actions.map(({ label, value }, index) => { From 54927454bf88dc73f55b8f56eb28ccc29ef69d04 Mon Sep 17 00:00:00 2001 From: Andrey Lyubavin Date: Mon, 5 May 2025 14:37:20 -0400 Subject: [PATCH 26/33] ui fetch private key if permission allows it --- .../src/hooks/api/certificates/queries.tsx | 19 +++++++++++++++- .../components/CertificateCertModal.tsx | 22 +++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/api/certificates/queries.tsx b/frontend/src/hooks/api/certificates/queries.tsx index 50c751c06..c53cef471 100644 --- a/frontend/src/hooks/api/certificates/queries.tsx +++ b/frontend/src/hooks/api/certificates/queries.tsx @@ -6,7 +6,8 @@ import { TCertificate } from "./types"; export const certKeys = { getCertById: (serialNumber: string) => [{ serialNumber }, "cert"], - getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"] + getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"], + getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"] }; export const useGetCert = (serialNumber: string) => { @@ -38,3 +39,19 @@ export const useGetCertBody = (serialNumber: string) => { enabled: Boolean(serialNumber) }); }; + +export const useGetCertBundle = (serialNumber: string) => { + return useQuery({ + queryKey: certKeys.getCertBundle(serialNumber), + queryFn: async () => { + const { data } = await apiRequest.get<{ + certificate: string; + certificateChain: string; + serialNumber: string; + privateKey: string; + }>(`/api/v1/pki/certificates/${serialNumber}/bundle`); + return data; + }, + enabled: Boolean(serialNumber) + }); +}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx index 01c79589c..081a59e67 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx @@ -3,6 +3,12 @@ import { useGetCertBody } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useProjectPermission +} from "@app/context"; +import { useGetCertBundle } from "@app/hooks/api/certificates/queries"; type Props = { popUp: UsePopUpState<["certificateCert"]>; @@ -10,10 +16,20 @@ type Props = { }; export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => { - const { data } = useGetCertBody( - (popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || "" + const { permission } = useProjectPermission(); + + const serialNumber = + (popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""; + + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates ); + const { data } = canReadPrivateKey + ? useGetCertBundle(serialNumber) + : useGetCertBody(serialNumber); + return ( { serialNumber={data.serialNumber} certificate={data.certificate} certificateChain={data.certificateChain} + // A hacky fix for typescript error + privateKey={(data as { privateKey?: string }).privateKey} /> ) : (
From b6e6a3c6bef2e3306cd6baf8ba8214dfcf7318d4 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 5 May 2025 14:50:54 -0400 Subject: [PATCH 27/33] docs changes --- docs/api-reference/endpoints/certificates/bundle.mdx | 8 ++++++++ .../endpoints/certificates/private-key.mdx | 4 ++++ docs/internals/permissions/project-permissions.mdx | 11 ++++++----- 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 docs/api-reference/endpoints/certificates/bundle.mdx create mode 100644 docs/api-reference/endpoints/certificates/private-key.mdx diff --git a/docs/api-reference/endpoints/certificates/bundle.mdx b/docs/api-reference/endpoints/certificates/bundle.mdx new file mode 100644 index 000000000..78cf7e073 --- /dev/null +++ b/docs/api-reference/endpoints/certificates/bundle.mdx @@ -0,0 +1,8 @@ +--- +title: "List" +openapi: "GET /api/v2/workspace/{slug}/bundle" +--- + + + You must have the certificate `read-private-key` permission in order to call this endpoint. + diff --git a/docs/api-reference/endpoints/certificates/private-key.mdx b/docs/api-reference/endpoints/certificates/private-key.mdx new file mode 100644 index 000000000..15056fceb --- /dev/null +++ b/docs/api-reference/endpoints/certificates/private-key.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/workspace/{slug}/private-key" +--- diff --git a/docs/internals/permissions/project-permissions.mdx b/docs/internals/permissions/project-permissions.mdx index 4e0c592cb..acf95485b 100644 --- a/docs/internals/permissions/project-permissions.mdx +++ b/docs/internals/permissions/project-permissions.mdx @@ -252,11 +252,12 @@ Supports conditions and permission inversion #### Subject: `certificates` -| Action | Description | -| -------- | ----------------------------- | -| `read` | View certificates | -| `create` | Issue new certificates | -| `delete` | Revoke or remove certificates | +| Action | Description | +| -------------------- | ----------------------------- | +| `read` | View certificates | +| `read-private-key` | Read certificate private key | +| `create` | Issue new certificates | +| `delete` | Revoke or remove certificates | #### Subject: `certificate-templates` From d28c87ee6732e2e3cf4c9daa10a817848afeec53 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 May 2025 11:56:49 -0700 Subject: [PATCH 28/33] fix: use dns lookup as fallback for dns resolve --- .../dynamic-secret/dynamic-secret-fns.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts index 05d492240..ed4ebc45f 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts @@ -24,8 +24,13 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) = if (net.isIPv4(el)) { exclusiveIps.push(el); } else { - const resolvedIps = await dns.resolve4(el); - exclusiveIps.push(...resolvedIps); + try { + const resolvedIps = await dns.resolve4(el); + exclusiveIps.push(...resolvedIps); + } catch { + const resolvedIps = (await dns.lookup(el, { all: true })).map(({ address }) => address); + exclusiveIps.push(...resolvedIps); + } } } } @@ -38,8 +43,13 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) = if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") { throw new BadRequestError({ message: "Invalid db host" }); } - const resolvedIps = await dns.resolve4(host); - inputHostIps.push(...resolvedIps); + try { + const resolvedIps = await dns.resolve4(host); + inputHostIps.push(...resolvedIps); + } catch { + const resolvedIps = (await dns.lookup(host, { all: true })).map(({ address }) => address); + inputHostIps.push(...resolvedIps); + } } if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) { From f6e802c017672192205c8d844db918e856f25765 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 5 May 2025 15:07:57 -0400 Subject: [PATCH 29/33] review fixes: docs + frontend --- .../endpoints/certificates/bundle.mdx | 2 +- .../endpoints/certificates/private-key.mdx | 2 +- .../components/CertificateCertModal.tsx | 26 ++++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/api-reference/endpoints/certificates/bundle.mdx b/docs/api-reference/endpoints/certificates/bundle.mdx index 78cf7e073..5fbda7d96 100644 --- a/docs/api-reference/endpoints/certificates/bundle.mdx +++ b/docs/api-reference/endpoints/certificates/bundle.mdx @@ -1,5 +1,5 @@ --- -title: "List" +title: "Get Certificate Bundle" openapi: "GET /api/v2/workspace/{slug}/bundle" --- diff --git a/docs/api-reference/endpoints/certificates/private-key.mdx b/docs/api-reference/endpoints/certificates/private-key.mdx index 15056fceb..244aecea3 100644 --- a/docs/api-reference/endpoints/certificates/private-key.mdx +++ b/docs/api-reference/endpoints/certificates/private-key.mdx @@ -1,4 +1,4 @@ --- -title: "List" +title: "Get Certificate Private Key" openapi: "GET /api/v2/workspace/{slug}/private-key" --- diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx index 081a59e67..54620f1d6 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx @@ -1,14 +1,14 @@ import { Modal, ModalContent } from "@app/components/v2"; -import { useGetCertBody } from "@app/hooks/api"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -import { CertificateContent } from "./CertificateContent"; import { ProjectPermissionCertificateActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { useGetCertBody } from "@app/hooks/api"; import { useGetCertBundle } from "@app/hooks/api/certificates/queries"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { CertificateContent } from "./CertificateContent"; type Props = { popUp: UsePopUpState<["certificateCert"]>; @@ -26,9 +26,18 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => { ProjectPermissionSub.Certificates ); - const { data } = canReadPrivateKey - ? useGetCertBundle(serialNumber) - : useGetCertBody(serialNumber); + // useGetCertBundle fails unless user has the correct permissions + const { data: bundleData } = useGetCertBundle(serialNumber); + const { data: bodyData } = useGetCertBody(serialNumber); + + const data: + | { + certificate: string; + certificateChain: string; + serialNumber: string; + privateKey?: string; + } + | undefined = canReadPrivateKey ? bundleData : bodyData; return ( { serialNumber={data.serialNumber} certificate={data.certificate} certificateChain={data.certificateChain} - // A hacky fix for typescript error - privateKey={(data as { privateKey?: string }).privateKey} + privateKey={data.privateKey} /> ) : (
From 7851bb8710d157bf8ddd88026011667032c13aae Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 May 2025 12:23:18 -0700 Subject: [PATCH 30/33] improvement: address feedback --- .../dynamic-secret/dynamic-secret-fns.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts index ed4ebc45f..3a4fecc3c 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts @@ -9,7 +9,7 @@ import { getDbConnectionHost } from "@app/lib/knex"; export const verifyHostInputValidity = async (host: string, isGateway = false) => { const appCfg = getConfig(); - if (appCfg.isDevelopmentMode) return [host]; + // if (appCfg.isDevelopmentMode) return [host]; const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat( (appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)), @@ -27,8 +27,11 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) = try { const resolvedIps = await dns.resolve4(el); exclusiveIps.push(...resolvedIps); - } catch { - const resolvedIps = (await dns.lookup(el, { all: true })).map(({ address }) => address); + } catch (error) { + // only try lookup if not found + if ((error as { code: string })?.code !== "ENOTFOUND") throw error; + + const resolvedIps = (await dns.lookup(el, { all: true, family: 4 })).map(({ address }) => address); exclusiveIps.push(...resolvedIps); } } @@ -46,8 +49,11 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) = try { const resolvedIps = await dns.resolve4(host); inputHostIps.push(...resolvedIps); - } catch { - const resolvedIps = (await dns.lookup(host, { all: true })).map(({ address }) => address); + } catch (error) { + // only try lookup if not found + if ((error as { code: string })?.code !== "ENOTFOUND") throw error; + + const resolvedIps = (await dns.lookup(host, { all: true, family: 4 })).map(({ address }) => address); inputHostIps.push(...resolvedIps); } } From 210f1dc2a2d4ff3092833076aa8e8f427d50cbcd Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 May 2025 12:24:12 -0700 Subject: [PATCH 31/33] fix: revert dev comment out --- backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts index 3a4fecc3c..f653d0c0c 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts @@ -9,7 +9,7 @@ import { getDbConnectionHost } from "@app/lib/knex"; export const verifyHostInputValidity = async (host: string, isGateway = false) => { const appCfg = getConfig(); - // if (appCfg.isDevelopmentMode) return [host]; + if (appCfg.isDevelopmentMode) return [host]; const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat( (appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)), From 6a79830e01286b1d99ff4199af3ed62c02dec4e4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 5 May 2025 17:32:18 -0400 Subject: [PATCH 32/33] Update bug-bounty.mdx --- docs/internals/bug-bounty.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/internals/bug-bounty.mdx b/docs/internals/bug-bounty.mdx index b823e6246..c5bc8b39e 100644 --- a/docs/internals/bug-bounty.mdx +++ b/docs/internals/bug-bounty.mdx @@ -41,7 +41,7 @@ All final reward amounts are determined at Infisical's discretion based on impac ### Out of Scope -- Social engineering or phishing +- Social engineering or phishing (including email hyperlink injection without code execution) - Rate limiting issues on non-sensitive endpoints - Denial-of-service attacks that require authentication and don't impact core service availability - Findings based on outdated or forked code not maintained by the Infisical team @@ -57,4 +57,4 @@ We ask that researchers: - Use testing accounts where possible - Give us a reasonable window to investigate and patch before going public -Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally. \ No newline at end of file +Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally. From 229c7c0dcfc0d14a26ed55dbd14ee20bb664596f Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 5 May 2025 20:43:45 -0400 Subject: [PATCH 33/33] Add missing HOST environment var Added missing HOST environment var --- docs/self-hosting/configuration/envars.mdx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index d9eef9cb0..ae7c4ab33 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -29,6 +29,19 @@ Used to configure platform-specific security and operational settings Specifies the internal port on which the application listens. + + Specifies the network interface Infisical will bind to when accepting incoming connections. + + By default, Infisical binds to `localhost`, which restricts access to connections from the same machine. + + To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces. + + Example values: + - `localhost` (default, same as `127.0.0.1`) + - `0.0.0.0` (all interfaces, accessible externally) + - `192.168.1.100` (specific interface IP) + + Telemetry helps us improve Infisical but if you want to disable it you may set this to `false`.