diff --git a/README.md b/README.md index 992c97c408..1a44117a49 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus ### Secrets Management: - **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface. -- **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more. +- **[Secret Syncs](https://infisical.com/docs/integrations/secret-syncs/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more. - **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed. - **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more. - **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more. @@ -56,13 +56,15 @@ We're on a mission to make security tooling more accessible to everyone, not jus - **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments. - **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)**: Inject secrets into applications without modifying any code logic. -### Infisical (Internal) PKI: +### Certificate Management -- **[Private Certificate Authority](https://infisical.com/docs/documentation/platform/pki/private-ca)**: Create CA hierarchies, configure [certificate templates](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-issuing-certificates) for policy enforcement, and start issuing X.509 certificates. -- **[Certificate Management](https://infisical.com/docs/documentation/platform/pki/certificates)**: Manage the certificate lifecycle from [issuance](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-issuing-certificates) to [revocation](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-revoking-certificates) with support for CRL. +- **[Internal CA](https://infisical.com/docs/documentation/platform/pki/private-ca)**: Create and manage a private + CA hierarchy directly within Infisical. +- **[External CA](https://infisical.com/docs/documentation/platform/pki/ca/external-ca)**: Integrate with third-party certificate authorities such as Let’s Encrypt, DigiCert, Microsoft AD CS, and more to leverage existing PKI infrastructure + or issue publicly trusted certificates. +- **[Certificate Lifecycle Management](https://infisical.com/docs/documentation/platform/pki/certificates/overview)**: Create certificate [profiles](https://infisical.com/docs/documentation/platform/pki/certificates/profiles) and [templates](https://infisical.com/docs/documentation/platform/pki/certificates/templates) to control how certificates are issued, including [enrollment methods](https://infisical.com/docs/documentation/platform/pki/enrollment-methods/overview) such as API, ACME, or EST. Manage the full lifecycle from issuance to renewal and [revocation](https://infisical.com/docs/documentation/platform/pki/certificates/certificates#guide-to-revoking-certificates) with CRL and inventory tracking. +- **[Certificate Syncs](https://infisical.com/docs/documentation/platform/pki/certificate-syncs/overview)**: Sync certificates to external platforms like [AWS Certificate Manager](https://infisical.com/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager) and [Azure Key Vault](https://infisical.com/docs/documentation/platform/pki/certificate-syncs/azure-key-vault). - **[Alerting](https://infisical.com/docs/documentation/platform/pki/alerting)**: Configure alerting for expiring CA and end-entity certificates. -- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal. -- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol. ### Infisical Key Management System (KMS): diff --git a/backend/package-lock.json b/backend/package-lock.json index 9cdaa764bb..cfe22d916e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -58,6 +58,7 @@ "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.2", "@slack/web-api": "^7.8.0", + "@types/node-forge": "^1.3.14", "@ucast/mongo2js": "^1.3.4", "acme-client": "^5.4.0", "ajv": "^8.12.0", @@ -97,6 +98,7 @@ "ms": "^2.1.3", "mysql2": "^3.9.8", "nanoid": "^3.3.8", + "node-forge": "^1.3.1", "nodemailer": "^6.9.9", "oci-sdk": "^2.108.0", "odbc": "^2.4.9", @@ -15272,6 +15274,15 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node/node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -33526,18 +33537,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-node/node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~7.16.0" - } - }, "node_modules/vite-node/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -33569,15 +33568,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vite-node/node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/vite-node/node_modules/vite": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", diff --git a/backend/package.json b/backend/package.json index fa4ee2f5f1..baa81e65be 100644 --- a/backend/package.json +++ b/backend/package.json @@ -185,6 +185,7 @@ "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.2", "@slack/web-api": "^7.8.0", + "@types/node-forge": "^1.3.14", "@ucast/mongo2js": "^1.3.4", "acme-client": "^5.4.0", "ajv": "^8.12.0", @@ -224,6 +225,7 @@ "ms": "^2.1.3", "mysql2": "^3.9.8", "nanoid": "^3.3.8", + "node-forge": "^1.3.1", "nodemailer": "^6.9.9", "oci-sdk": "^2.108.0", "odbc": "^2.4.9", 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 bcc2a07703..ba409a4fba 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -323,6 +323,7 @@ export enum EventType { GET_CERT_BODY = "get-cert-body", GET_CERT_PRIVATE_KEY = "get-cert-private-key", GET_CERT_BUNDLE = "get-cert-bundle", + EXPORT_CERT_PKCS12 = "export-cert-pkcs12", CREATE_PKI_ALERT = "create-pki-alert", GET_PKI_ALERT = "get-pki-alert", UPDATE_PKI_ALERT = "update-pki-alert", @@ -2315,6 +2316,14 @@ interface GetCertBundle { serialNumber: string; }; } +interface GetCertPkcs12 { + type: EventType.EXPORT_CERT_PKCS12; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} interface CreatePkiAlert { type: EventType.CREATE_PKI_ALERT; @@ -4252,6 +4261,7 @@ export type Event = | GetCertBody | GetCertPrivateKey | GetCertBundle + | GetCertPkcs12 | CreatePkiAlert | GetPkiAlert | UpdatePkiAlert diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index 413990c5d4..7dd7948ef6 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -104,8 +104,7 @@ const makeSqlConnection = ( // (like being able to do an auth handshake regardless pass or not) if ( connectOnly && - (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` || - error.message.includes("no pg_hba.conf entry for host")) + error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ) { return; } diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 31f3139ec9..e6ba2eec74 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -182,8 +182,8 @@ export const injectIdentity = fp( case AuthMode.IDENTITY_ACCESS_TOKEN: { const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken( token, - subOrganizationSelector, - req.realIp + req.realIp, + subOrganizationSelector ); const serverCfg = await getServerCfg(); requestContext.set("orgId", identity.orgId); diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 443d64e227..e8cdbb5404 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ +import RE2 from "re2"; import { z } from "zod"; import { CertificatesSchema } from "@app/db/schemas"; @@ -616,4 +617,64 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/:serialNumber/pkcs12", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + tags: [ApiDocsTags.PkiCertificates], + description: "Download certificate in PKCS12 format", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + }), + body: z.object({ + password: z + .string() + .min(6, "Password must be at least 6 characters long") + .describe("Password for the keystore (minimum 6 characters)"), + alias: z.string().min(1, "Alias is required").describe("Alias for the certificate in the keystore") + }), + response: { + 200: z.any().describe("PKCS12 keystore as binary data") + } + }, + handler: async (req, reply) => { + const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({ + serialNumber: req.params.serialNumber, + password: req.body.password, + alias: req.body.alias, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.EXPORT_CERT_PKCS12, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + addNoCacheHeaders(reply); + reply.header("Content-Type", "application/octet-stream"); + reply.header( + "Content-Disposition", + `attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"` + ); + + return pkcs12Data; + } + }); }; diff --git a/backend/src/server/routes/v1/pki-alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts index fde1799817..60a906c3aa 100644 --- a/backend/src/server/routes/v1/pki-alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -17,7 +17,6 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiAlerting], description: "Create PKI alert", body: z.object({ @@ -72,7 +71,6 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiAlerting], description: "Get PKI alert", params: z.object({ @@ -114,7 +112,6 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiAlerting], description: "Update PKI alert", params: z.object({ @@ -173,7 +170,6 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiAlerting], description: "Delete PKI alert", params: z.object({ diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index b2bc4df41a..d2d2aa3ceb 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -1,4 +1,5 @@ import * as x509 from "@peculiar/x509"; +import forge from "node-forge"; import RE2 from "re2"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -104,3 +105,53 @@ export const getCertificateCredentials = async ({ throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` }); } }; + +export const generatePkcs12FromCertificate = async ({ + certificate, + certificateChain, + privateKey, + password, + alias +}: { + certificate: string; + certificateChain: string; + privateKey: string; + password: string; + alias: string; +}): Promise => { + try { + if (!password || password.trim() === "") { + throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" }); + } + + const cert = forge.pki.certificateFromPem(certificate); + const key = forge.pki.privateKeyFromPem(privateKey); + + const chainCerts = []; + if (certificateChain) { + const chainPems = splitPemChain(certificateChain); + for (const chainPem of chainPems) { + try { + const chainCert = forge.pki.certificateFromPem(chainPem); + chainCerts.push(chainCert); + } catch (error) { + // Skip invalid certificates in chain + } + } + } + + // Generate PKCS12 file + const p12Asn1 = forge.pkcs12.toPkcs12Asn1(key, [cert, ...chainCerts], password, { + algorithm: "aes256", // Modern AES-256 encryption + friendlyName: alias + }); + + const p12Der = forge.asn1.toDer(p12Asn1).getBytes(); + + return Buffer.from(p12Der, "binary"); + } catch (error) { + throw new BadRequestError({ + message: `Failed to generate PKCS12 keystore: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } +}; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index eb8006f00d..b632e76fb3 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -29,7 +29,12 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; -import { getCertificateCredentials, revocationReasonToCrlCode, splitPemChain } from "./certificate-fns"; +import { + generatePkcs12FromCertificate, + getCertificateCredentials, + revocationReasonToCrlCode, + splitPemChain +} from "./certificate-fns"; import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; import { CertExtendedKeyUsage, @@ -40,6 +45,7 @@ import { TGetCertBodyDTO, TGetCertBundleDTO, TGetCertDTO, + TGetCertPkcs12DTO, TGetCertPrivateKeyDTO, TImportCertDTO, TRevokeCertDTO @@ -656,6 +662,71 @@ export const certificateServiceFactory = ({ }; }; + const getCertPkcs12 = async ({ + serialNumber, + password, + alias, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetCertPkcs12DTO) => { + if (!password || password.trim() === "") { + throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" }); + } + + if (password.length < 6) { + throw new BadRequestError({ + message: "Password must be at least 6 characters long for PKCS12 keystore security" + }); + } + + if (!alias || alias.trim() === "") { + throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" }); + } + const cert = await certificateDAL.findOne({ serialNumber }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: cert.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + + // Get certificate bundle (certificate, chain, private key) + const { certificate, certificateChain, privateKey } = await getCertBundle({ + serialNumber, + actor, + actorId, + actorAuthMethod, + actorOrgId + }); + + if (!privateKey) { + throw new BadRequestError({ message: "Certificate private key is required for PKCS12 export" }); + } + + const pkcs12Data = await generatePkcs12FromCertificate({ + certificate, + certificateChain: certificateChain || "", + privateKey, + password, + alias + }); + + return { + pkcs12Data, + cert + }; + }; + return { getCert, getCertPrivateKey, @@ -663,6 +734,7 @@ export const certificateServiceFactory = ({ revokeCert, getCertBody, importCert, - getCertBundle + getCertBundle, + getCertPkcs12 }; }; diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index d654c96ba8..085bb9588c 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -119,6 +119,12 @@ export type TGetCertBundleDTO = { serialNumber: string; } & Omit; +export type TGetCertPkcs12DTO = { + serialNumber: string; + password: string; + alias: string; +} & Omit; + export type TGetCertificateCredentialsDTO = { certId: string; projectId: string; diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 3479d929e4..c720c97c0f 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -186,8 +186,8 @@ export const identityAccessTokenServiceFactory = ({ const fnValidateIdentityAccessToken = async ( token: TIdentityAccessTokenJwtPayload, - subOrganizationSelector?: string, - ipAddress?: string + ipAddress?: string, + subOrganizationSelector?: string ) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts index 3e07420b5e..af210c0c5d 100644 --- a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts @@ -1,5 +1,5 @@ /* eslint-disable no-await-in-loop */ -import * as AWS from "aws-sdk"; +import AWS from "aws-sdk"; import RE2 from "re2"; import { z } from "zod"; diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts index 8b2b8b87e3..e86ecaab9f 100644 --- a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts @@ -1,4 +1,4 @@ -import * as AWS from "aws-sdk"; +import AWS from "aws-sdk"; import { z } from "zod"; import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; diff --git a/docs/api-reference/endpoints/pki-alerts/create.mdx b/docs/api-reference/endpoints/pki-alerts/create.mdx index 458f0cd483..d4be026a79 100644 --- a/docs/api-reference/endpoints/pki-alerts/create.mdx +++ b/docs/api-reference/endpoints/pki-alerts/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/pki/alerts" +openapi: "POST /api/v2/pki/alerts" --- diff --git a/docs/api-reference/endpoints/pki-alerts/delete.mdx b/docs/api-reference/endpoints/pki-alerts/delete.mdx index c0918d1fea..67429049cf 100644 --- a/docs/api-reference/endpoints/pki-alerts/delete.mdx +++ b/docs/api-reference/endpoints/pki-alerts/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/pki/alerts/{alertId}" +openapi: "DELETE /api/v2/pki/alerts/{alertId}" --- diff --git a/docs/api-reference/endpoints/pki-alerts/read.mdx b/docs/api-reference/endpoints/pki-alerts/read.mdx index 928afdbc5d..0e05472887 100644 --- a/docs/api-reference/endpoints/pki-alerts/read.mdx +++ b/docs/api-reference/endpoints/pki-alerts/read.mdx @@ -1,4 +1,4 @@ --- title: "Retrieve" -openapi: "GET /api/v1/pki/alerts/{alertId}" +openapi: "GET /api/v2/pki/alerts/{alertId}" --- diff --git a/docs/api-reference/endpoints/pki-alerts/update.mdx b/docs/api-reference/endpoints/pki-alerts/update.mdx index 829f8c57b9..45f1f1f1f8 100644 --- a/docs/api-reference/endpoints/pki-alerts/update.mdx +++ b/docs/api-reference/endpoints/pki-alerts/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/pki/alerts/{alertId}" +openapi: "PATCH /api/v2/pki/alerts/{alertId}" --- diff --git a/docs/docs.json b/docs/docs.json index db4c525270..db61799dc0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -702,7 +702,7 @@ ] }, { - "item": "Infisical PKI", + "item": "Certificate Management", "groups": [ { "group": "Certificate Management", @@ -2625,15 +2625,6 @@ "api-reference/endpoints/pki-collections/delete-item" ] }, - { - "group": "PKI Alerting", - "pages": [ - "api-reference/endpoints/pki-alerts/create", - "api-reference/endpoints/pki-alerts/read", - "api-reference/endpoints/pki-alerts/update", - "api-reference/endpoints/pki-alerts/delete" - ] - }, { "group": "Certificate Profiles", "pages": [ diff --git a/docs/documentation/getting-started/overview.mdx b/docs/documentation/getting-started/overview.mdx index 1ebb2ff9d7..e0b0788cad 100644 --- a/docs/documentation/getting-started/overview.mdx +++ b/docs/documentation/getting-started/overview.mdx @@ -29,10 +29,10 @@ description: "The open source platform for managing secrets, certificates, and s Automatically detect and alert on hardcoded secrets in source code, CI pipelines, and infrastructure. - Automate the issuance and management of X.509 certificates across your infrastructure using modern protocols like EST. + Automate CA and X.509 certificate lifecycle management across your infrastructure. - Encrypt and decrypt sensitive data using a centralized key management system. + Encrypt and decrypt sensitive data using a centralized key management system. ## Resources - + Explore Infisical’s command-line interface for managing secrets, certificates, and system operations via terminal. - + Browse Infisical’s API documentation to programmatically interact with secrets, access controls, and certificate workflows. - - Learn how to deploy and operate Infisical on your own infrastructure with full - control and data ownership. - + + Learn how to deploy and operate Infisical on your own infrastructure with + full control and data ownership. + diff --git a/docs/documentation/platform/pki/alerting.mdx b/docs/documentation/platform/pki/alerting.mdx index 158c478770..c2342e0205 100644 --- a/docs/documentation/platform/pki/alerting.mdx +++ b/docs/documentation/platform/pki/alerting.mdx @@ -5,145 +5,21 @@ description: "Learn how to set up alerting for expiring certificates with Infisi ## Concept -In order to ensure that your certificates are always up-to-date and not expired, you can set up alerting for expiring CA and leaf certificates in Infisical. - -## Workflow - -A typical alerting workflow for expiring certificates consists of the following steps: - -1. Creating a PKI/Certificate collection and adding certificates that you wish to monitor for expiration to it. -2. Creating an alert and binding it to the PKI/Certificate collection. As part of the configuration, you specify when the alert should trigger based on the number of days before certificate expiration and the email addresses of the recipients to notify. +In order to ensure that your certificates are always up-to-date and not expired, you can set up alerting in Infisical for expiring CA and leaf certificates based on customizable filters. ## Guide to Creating an Alert - - - - - To create a PKI/Certificate collection, head to your Project > Internal - PKI > Alerting > Certificate Collection and press **Create**. - - ![pki create collection](/images/platform/pki/alerting/collection-create.png) - - Give the collection a name and proceed to create the empty collection. +To create an alert, head to your Certificate Management Project > Alerting and press **Create Certificate Alert**. - ![pki create collection](/images/platform/pki/alerting/collection-create-2.png) +![pki alerting](/images/platform/pki/alerting/alert-create.png) - Next, in the Collection Page, add the certificate authorities and leaf certificates - that you wish to monitor for expiration to the collection. +![pki alerting modal](/images/platform/pki/alerting/alert-create-modal.png) - ![pki add cert to collection](/images/platform/pki/alerting/collection-add-cert.png) - - - To create an alert, head to your Project > Internal PKI > Alerting > Alerts and press **Create**. +Here's some guidance for each field in the alert configuration sequence: - ![pki create alert](/images/platform/pki/alerting/alert-create.png) - - Here, set the **Certificate Collection** to the PKI/Certificate collection you created in the previous step and fill out details for the alert. - - ![pki create alert](/images/platform/pki/alerting/alert-create-2.png) - - Here's some guidance on each field: - - - Name: A name for the alert. - - Collection Collection: The PKI/Certificate collection to bind the alert to from the previous step. - - Alert Before / Unit: The time before certificate expiration to trigger the alert. - - Emails to Alert: A comma-delimited list of email addresses to notify when the alert triggers. - - Finally, press **Create** to create the alert. - - ![pki alerts](/images/platform/pki/alerting/alerts.png) - - Great! You've successfully created a PKI/Certificate collection and an alert to monitor the expiring certificates in the collection. Once the alert triggers, the specified email addresses will be notified. - - - - - - - - 1.1. To create a PKI/Certificate collection, make an API request to the [Create PKI Collection](/api-reference/endpoints/pki-collections/create) API endpoint. - - ### Sample request - - ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/collections' \ - --header 'Authorization: Bearer ' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "projectId": "", - "name": "My Certificate Collection" - }' - ``` - - ### Sample response - - ```bash Response - { - id: "", - name: "My Certificate Collection", - ... - } - ``` - - 1.2. Next, make an API request to the [Add Collection Item](/api-reference/endpoints/pki-collections/add-item) API endpoint to add a certificate to the collection. - - ### Sample request - - ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/collections//items' \ - --header 'Authorization: Bearer ' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "type": "certificate", - "itemId": "id-of-certificate" - }' - ``` - - ### Sample response - - ```bash Response - { - id: "", - type: "certificate", - itemId: "id-of-certificate" - ... - } - ``` - - - To create an alert, make an API request to the [Create Alert](/api-reference/endpoints/pki-alerts/create) API endpoint, specifying the PKI/Certificate collection to bind the alert to, the alert configuration, and the email addresses to notify. - - ### Sample request - - ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/alerts' \ - --header 'Authorization: Bearer ' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "projectId": "", - "pkiCollectionId": "", - "name": "My Alert", - "alertBeforeDays": 30, - "emails": ["johndoe@gmail.com", "janedoe@gmail.com"] - }' - ``` - - ### Sample response - - ```bash Response - { - id: "", - name: "My Alert", - alertBeforeDays: 30, - recipientEmails: "johndoe@gmail.com,janedoe@gmail.com" - ... - } - ``` - - Great! You've successfully created a PKI/Certificate collection and an alert to monitor the expiring certificate in the collection. Once the alert triggers, the specified email addresses will be notified. - - - - - +- Alert Type: The type of alert to create such as **Certificate Expiration**. +- Alert Name: A slug-friendly name for the alert such as `tls-expiry-alert`. +- Description: An optional description for the alert. +- Alert Before: The time before certificate expiration to trigger the alert such as 30 days denoted by `30d`. +- Filters: A list of filters that determine which certificates the alert applies to. Each row includes a **Field**, **Operator**, and **Value** to match against. For example, you can filter for certificates with a common name containing `example.com` by setting the field to **Common Name**, the operator to **Contains**, and the value to `example.com`. +- Channels / Email Recipients: A list of email addresses to notify when the alert triggers. diff --git a/docs/documentation/platform/pki/certificates/certificates.mdx b/docs/documentation/platform/pki/certificates/certificates.mdx index b30a53091e..3297022e8f 100644 --- a/docs/documentation/platform/pki/certificates/certificates.mdx +++ b/docs/documentation/platform/pki/certificates/certificates.mdx @@ -60,6 +60,107 @@ The following examples demonstrate different approaches to certificate renewal: - Using the ACME enrollment method, you may use [cert-manager](https://cert-manager.io/) with Infisical to issue and renew certificates for Kubernetes workloads; cert-manager will pursue a client-driven approach and submit certificate requests upon certificate expiration for you, saving renewed certificates back to Kubernetes secrets. - Using the API enrollment method, you may push and auto-renew certificates to AWS and Azure using [certificate syncs](/documentation/platform/pki/certificate-syncs/overview). Certificates issued over the API enrollment method, where key pairs are generated server-side, are also eligible for server-side auto-renewal; once renewed, certificates are automatically pushed back to their sync destination. +## Guide to Exporting Certificates + +In the following steps, we explore how to export certificates from Infisical in different formats for use in your applications and infrastructure. + +### Accessing the Export Certificate Modal + +To export any certificate, first navigate to your project's certificate inventory and locate the certificate you want to export. Click on the **Export Certificate** option from the certificate's action menu. + +![pki export certificate option](/images/platform/pki/certificate/cert-export-option.png) + + + + + + In the export modal, choose **PEM** as the format and click **Export**. + + ![pki export certificate pem](/images/platform/pki/certificate/cert-export-pem.png) + + The PEM export modal will display the certificate details including: + - **Serial Number**: The unique identifier for the certificate + - **Certificate Body**: The X.509 certificate in PEM format + - **Certificate Chain**: The intermediate and root CA certificates + - **Private Key**: The private key associated with the certificate (if available) + + ![pki export certificate pem modal](/images/platform/pki/certificate/cert-export-pem-modal.png) + + You can copy each component individually or use the **Copy All** button to copy the complete certificate bundle. + + + PEM format certificates can be used directly with most web servers and applications: + + - **Apache HTTP Server**: Configure SSL certificates in your virtual host + - **Nginx**: Use the certificate and private key files in your server configuration + - **Docker containers**: Mount certificate files for TLS-enabled applications + - **Load balancers**: Upload PEM certificates to AWS ALB, Azure Application Gateway, etc. + + Example Nginx configuration: + ```nginx + server { + listen 443 ssl; + server_name example.com; + + ssl_certificate /path/to/certificate.pem; + ssl_certificate_key /path/to/private-key.pem; + } + ``` + + + + + + + In the export modal, choose **PKCS12** as the format and provide the required configuration: + + ![pki export certificate pkcs12](/images/platform/pki/certificate/cert-export-pkcs12.png) + + - **Password**: A secure password to protect the PKCS12 keystore + - **Alias**: A friendly name for the certificate within the keystore + + Click **Export** to generate and download the `.p12` file containing the certificate, certificate chain, and private key. + + + PKCS12 files (`.p12` extension) are binary keystore files that contain the certificate, certificate chain, and private key in a single encrypted file: + + - **Java applications**: Import directly into Java KeyStore (JKS) or use with SSL/TLS + - **Windows IIS**: Import the PKCS12 file for web server SSL configuration + - **Browser certificates**: Install client certificates for authentication + - **Mobile applications**: Deploy certificates to iOS and Android applications + + To verify the contents of a PKCS12 file: + ```bash + openssl pkcs12 -in certificate.p12 -nokeys -clcerts + ``` + + To extract the private key: + ```bash + openssl pkcs12 -in certificate.p12 -nocerts -out private-key.pem + ``` + + + If you need to convert the PKCS12 file to Java KeyStore (JKS) format for applications running on Java 8 or earlier, use the following keytool command: + + ```bash + keytool -importkeystore \ + -srckeystore certificate.p12 \ + -srcstoretype PKCS12 \ + -srcstorepass \ + -destkeystore certificate.jks \ + -deststoretype JKS \ + -deststorepass + ``` + + Replace `` with the password you used when exporting the PKCS12 file, and `` with your desired JKS keystore password. + + The resulting `.jks` file can then be used with Java applications that require JKS format keystores. + + + + + + ## Guide to Revoking Certificates In the following steps, we explore how to revoke a X.509 certificate and obtain a Certificate Revocation List (CRL) for a CA. diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index d31bd96d9e..9aba032b1f 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -1,7 +1,7 @@ --- -title: "Infisical PKI" +title: "Certificate Management" sidebarTitle: "Overview" -description: "Learn how to create a Private CA hierarchy and issue X.509 certificates." +description: "Manage Certificate Authorities and automate X.509 certificate lifecycle management." --- Infisical can be used to create and manage Certificate Authorities (CAs) and issue digital X.509 certificates. This allows you to manage PKI infrastructure and issue certificates for end-entities such as load balancers, web servers, devices, and more. diff --git a/docs/images/platform/pki/alerting/alert-create-2.png b/docs/images/platform/pki/alerting/alert-create-2.png deleted file mode 100644 index 812f253f81..0000000000 Binary files a/docs/images/platform/pki/alerting/alert-create-2.png and /dev/null differ diff --git a/docs/images/platform/pki/alerting/alert-create-modal.png b/docs/images/platform/pki/alerting/alert-create-modal.png new file mode 100644 index 0000000000..f65530ce4e Binary files /dev/null and b/docs/images/platform/pki/alerting/alert-create-modal.png differ diff --git a/docs/images/platform/pki/alerting/alert-create.png b/docs/images/platform/pki/alerting/alert-create.png index 4a7b3227fd..6204842233 100644 Binary files a/docs/images/platform/pki/alerting/alert-create.png and b/docs/images/platform/pki/alerting/alert-create.png differ diff --git a/docs/images/platform/pki/alerting/alerts.png b/docs/images/platform/pki/alerting/alerts.png deleted file mode 100644 index c7a5096ceb..0000000000 Binary files a/docs/images/platform/pki/alerting/alerts.png and /dev/null differ diff --git a/docs/images/platform/pki/alerting/collection-add-cert.png b/docs/images/platform/pki/alerting/collection-add-cert.png deleted file mode 100644 index 6300cc8923..0000000000 Binary files a/docs/images/platform/pki/alerting/collection-add-cert.png and /dev/null differ diff --git a/docs/images/platform/pki/alerting/collection-create-2.png b/docs/images/platform/pki/alerting/collection-create-2.png deleted file mode 100644 index 53378ef053..0000000000 Binary files a/docs/images/platform/pki/alerting/collection-create-2.png and /dev/null differ diff --git a/docs/images/platform/pki/alerting/collection-create.png b/docs/images/platform/pki/alerting/collection-create.png deleted file mode 100644 index 7c4883201f..0000000000 Binary files a/docs/images/platform/pki/alerting/collection-create.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate/cert-export-option.png b/docs/images/platform/pki/certificate/cert-export-option.png new file mode 100644 index 0000000000..ae966905c0 Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-export-option.png differ diff --git a/docs/images/platform/pki/certificate/cert-export-pem-modal.png b/docs/images/platform/pki/certificate/cert-export-pem-modal.png new file mode 100644 index 0000000000..df01db0bfb Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-export-pem-modal.png differ diff --git a/docs/images/platform/pki/certificate/cert-export-pem.png b/docs/images/platform/pki/certificate/cert-export-pem.png new file mode 100644 index 0000000000..927227394e Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-export-pem.png differ diff --git a/docs/images/platform/pki/certificate/cert-export-pkcs12.png b/docs/images/platform/pki/certificate/cert-export-pkcs12.png new file mode 100644 index 0000000000..773c69c74e Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-export-pkcs12.png differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e8fdda0965..8965ce45e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -150,8 +150,7 @@ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", @@ -196,6 +195,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -488,6 +488,7 @@ "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.2.tgz", "integrity": "sha512-KjKXlcjKbUz8dKw7PY56F7qlfOFgxTU6tnlJ8YrbDyWkJMIlHa6VRWzCD8RU20zbJUC1hExhOFggZjm6tf1mUw==", "license": "MIT", + "peer": true, "dependencies": { "@ucast/mongo2js": "^1.3.0" }, @@ -546,6 +547,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1323,6 +1325,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz", "integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.1" }, @@ -2013,6 +2016,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -4408,6 +4412,7 @@ "integrity": "sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/utils": "^8.13.0", "eslint-visitor-keys": "^4.2.0", @@ -5033,6 +5038,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.95.1.tgz", "integrity": "sha512-P5x4yNhcdkYsCEoYeGZP8Q9Jlxf0WXJa4G/xvbmM905seZc9FqJqvCSRvX3dWTPOXRABhl4g+8DHqfft0c/AvQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.95.0", "@tanstack/react-store": "^0.7.0", @@ -5244,7 +5250,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5265,7 +5270,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -5276,7 +5280,6 @@ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -5296,8 +5299,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@testing-library/user-event": { "version": "14.6.1", @@ -5305,7 +5307,6 @@ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12", "npm": ">=6" @@ -5326,8 +5327,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5380,7 +5380,6 @@ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -5454,8 +5453,7 @@ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/doctrine": { "version": "0.0.9", @@ -5591,6 +5589,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz", "integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5602,6 +5601,7 @@ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5649,6 +5649,7 @@ "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.0", @@ -5689,6 +5690,7 @@ "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", @@ -5960,7 +5962,6 @@ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", @@ -5978,7 +5979,6 @@ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", @@ -6006,7 +6006,6 @@ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -6017,7 +6016,6 @@ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tinyrainbow": "^2.0.0" }, @@ -6031,7 +6029,6 @@ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tinyspy": "^4.0.3" }, @@ -6045,7 +6042,6 @@ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", @@ -6119,6 +6115,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6235,7 +6232,6 @@ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">= 0.4" } @@ -6284,7 +6280,6 @@ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6365,7 +6360,6 @@ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6453,7 +6447,6 @@ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" } @@ -6464,7 +6457,6 @@ "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.0.1" }, @@ -6477,8 +6469,7 @@ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", @@ -6528,7 +6519,6 @@ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">= 0.4" } @@ -6630,7 +6620,6 @@ "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "open": "^8.0.4" }, @@ -6867,6 +6856,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -7048,7 +7038,6 @@ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -7119,7 +7108,6 @@ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 16" } @@ -7505,8 +7493,7 @@ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cssesc": { "version": "3.0.0", @@ -7525,7 +7512,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cva": { "name": "class-variance-authority", @@ -7597,6 +7585,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -7650,8 +7639,7 @@ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true, - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/data-view-buffer": { "version": "1.0.1", @@ -7761,7 +7749,6 @@ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -7796,7 +7783,6 @@ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7918,8 +7904,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -8129,7 +8114,6 @@ "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -8213,6 +8197,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8254,7 +8239,6 @@ "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.3.4" }, @@ -8291,6 +8275,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8347,6 +8332,7 @@ "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", @@ -8369,6 +8355,7 @@ "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "confusing-browser-globals": "^1.0.10", "object.assign": "^4.1.2", @@ -8399,6 +8386,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8498,6 +8486,7 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -8627,7 +8616,6 @@ "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -8661,6 +8649,7 @@ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -8684,7 +8673,6 @@ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -8698,7 +8686,6 @@ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -8717,7 +8704,6 @@ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -8879,7 +8865,6 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -9910,6 +9895,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" }, @@ -10003,7 +9989,6 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -10280,7 +10265,6 @@ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "is-docker": "cli.js" }, @@ -10602,7 +10586,6 @@ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -10638,7 +10621,6 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", - "peer": true, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -10650,7 +10632,6 @@ "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -10800,7 +10781,6 @@ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -10835,8 +10815,7 @@ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", "dev": true, - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", @@ -10844,7 +10823,6 @@ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -10877,7 +10855,6 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.102.tgz", "integrity": "sha512-g70kydI0I1sZU0ChO8mBbhw0oUW/8U0GHzygpvEIx8k+jgOpqnTSb/E+70toYVqHxBhrERD21TwD5QcZJQ40ZQ==", "license": "MIT", - "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -11202,8 +11179,7 @@ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -11230,7 +11206,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11922,7 +11897,6 @@ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -12271,7 +12245,6 @@ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -12541,7 +12514,6 @@ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 14.16" } @@ -12693,6 +12665,7 @@ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12809,7 +12782,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12825,7 +12797,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -12838,8 +12809,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prismjs": { "version": "1.30.0", @@ -13040,6 +13010,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13067,6 +13038,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -13158,6 +13130,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13208,6 +13181,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz", "integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13458,7 +13432,6 @@ "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", @@ -13476,7 +13449,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13493,7 +13465,6 @@ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -13508,7 +13479,6 @@ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "min-indent": "^1.0.0" }, @@ -13723,6 +13693,7 @@ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -14233,7 +14204,6 @@ "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -14249,7 +14219,6 @@ "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -14277,7 +14246,6 @@ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -14498,7 +14466,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.2.1", @@ -14602,7 +14571,6 @@ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14613,7 +14581,6 @@ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14898,6 +14865,7 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15283,6 +15251,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15700,7 +15669,6 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -15745,6 +15713,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -15895,6 +15864,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/hooks/api/certificates/index.tsx b/frontend/src/hooks/api/certificates/index.tsx index 7d9c5df084..bc7f80d2cb 100644 --- a/frontend/src/hooks/api/certificates/index.tsx +++ b/frontend/src/hooks/api/certificates/index.tsx @@ -1,6 +1,7 @@ export { CertStatus } from "./enums"; export { useDeleteCert, + useDownloadCertPkcs12, useImportCertificate, useRenewCertificate, useRevokeCert, diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index 2ee4705515..a6daf04917 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -7,6 +7,7 @@ import { projectKeys } from "../projects"; import { TCertificate, TDeleteCertDTO, + TDownloadPkcs12DTO, TImportCertificateDTO, TImportCertificateResponse, TRenewCertificateDTO, @@ -134,3 +135,42 @@ export const useUpdateRenewalConfig = () => { } }); }; + +export const useDownloadCertPkcs12 = () => { + return useMutation({ + mutationFn: async ({ serialNumber, projectSlug, password, alias }) => { + try { + const response = await apiRequest.post( + `/api/v1/pki/certificates/${serialNumber}/pkcs12`, + { + password, + alias + }, + { + params: { projectSlug }, + responseType: "arraybuffer" + } + ); + + // Create blob and trigger download + const blob = new Blob([response.data], { type: "application/octet-stream" }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `certificate-${serialNumber}.p12`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error: any) { + if (error.response?.data instanceof ArrayBuffer) { + const decoder = new TextDecoder(); + const errorText = decoder.decode(error.response.data); + const errorData = JSON.parse(errorText); + throw new Error(errorData.message); + } + throw error; + } + } + }); +}; diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index adfb815a6c..5e950b9c9a 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -72,3 +72,10 @@ export type TUpdateRenewalConfigDTO = { enableAutoRenewal?: boolean; projectSlug: string; }; + +export type TDownloadPkcs12DTO = { + serialNumber: string; + projectSlug: string; + password: string; + alias: string; +}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateExportModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateExportModal.tsx new file mode 100644 index 0000000000..3916477a15 --- /dev/null +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateExportModal.tsx @@ -0,0 +1,163 @@ +import { useEffect, useState } from "react"; +import { faDownload } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + popUp: UsePopUpState<["certificateExport"]>; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["certificateExport"]>, + state?: boolean + ) => void; + onFormatSelected: ( + format: "pem" | "pkcs12", + serialNumber: string, + options?: ExportOptions + ) => void; +}; + +export type CertificateExportFormat = "pem" | "pkcs12"; + +export type ExportOptions = { + pkcs12?: { + password: string; + alias: string; + }; +}; + +export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => { + const [selectedFormat, setSelectedFormat] = useState("pem"); + const [pkcs12Options, setPkcs12Options] = useState({ + password: "", + alias: "" + }); + + const serialNumber = + (popUp?.certificateExport?.data as { serialNumber: string })?.serialNumber || ""; + + // Reset form whenever the modal opens + useEffect(() => { + if (popUp?.certificateExport?.isOpen) { + setSelectedFormat("pem"); + setPkcs12Options({ + password: "", + alias: "" + }); + } + }, [popUp?.certificateExport?.isOpen]); + + const isFormValid = () => { + if (selectedFormat === "pkcs12") { + return pkcs12Options.password.length >= 6 && pkcs12Options.alias.trim() !== ""; + } + return true; + }; + + const handleExport = () => { + if (serialNumber && isFormValid()) { + const options: ExportOptions = {}; + + if (selectedFormat === "pkcs12") { + options.pkcs12 = pkcs12Options; + } + + onFormatSelected(selectedFormat, serialNumber, options); + handlePopUpToggle("certificateExport", false); + } + }; + + return ( + { + handlePopUpToggle("certificateExport", isOpen); + }} + > + +
+

Choose the format for exporting your certificate

+ + + + + + {selectedFormat === "pkcs12" && ( + <> + 0 && pkcs12Options.password.length < 6 + ? undefined + : "Password to protect the PKCS12 keystore (minimum 6 characters)" + } + isError={pkcs12Options.password.length > 0 && pkcs12Options.password.length < 6} + errorText="Password must be at least 6 characters long" + > + + setPkcs12Options((prev) => ({ ...prev, password: e.target.value })) + } + type="password" + /> + + + + setPkcs12Options((prev) => ({ ...prev, alias: e.target.value }))} + /> + + + )} + +
+ + +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index 6a5566d6a7..7ca9b81d01 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -9,10 +9,11 @@ import { ProjectPermissionSub, useProject } from "@app/context"; -import { useDeleteCert } from "@app/hooks/api"; +import { useDeleteCert, useDownloadCertPkcs12 } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { CertificateCertModal } from "./CertificateCertModal"; +import { CertificateExportModal, ExportOptions } from "./CertificateExportModal"; import { CertificateImportModal } from "./CertificateImportModal"; import { CertificateIssuanceModal } from "./CertificateIssuanceModal"; import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal"; @@ -24,11 +25,13 @@ import { CertificatesTable } from "./CertificatesTable"; export const CertificatesSection = () => { const { currentProject } = useProject(); const { mutateAsync: deleteCert } = useDeleteCert(); + const { mutateAsync: downloadCertPkcs12 } = useDownloadCertPkcs12(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "issueCertificate", "certificateImport", "certificateCert", + "certificateExport", "deleteCertificate", "revokeCertificate", "manageRenewal", @@ -49,6 +52,45 @@ export const CertificatesSection = () => { handlePopUpClose("deleteCertificate"); }; + const handleCertificateExport = async ( + format: "pem" | "pkcs12", + serialNumber: string, + options?: ExportOptions + ) => { + if (format === "pem") { + handlePopUpOpen("certificateCert", { serialNumber }); + } else if (format === "pkcs12") { + if (!currentProject?.slug) return; + + if (!options?.pkcs12?.password || !options?.pkcs12?.alias) { + createNotification({ + text: "Password and alias are required for PKCS12 export", + type: "error" + }); + return; + } + + try { + await downloadCertPkcs12({ + serialNumber, + projectSlug: currentProject.slug, + password: options.pkcs12.password, + alias: options.pkcs12.alias + }); + + createNotification({ + text: "PKCS12 certificate downloaded successfully", + type: "success" + }); + } catch (error: any) { + createNotification({ + text: error?.message || "Failed to download PKCS12 certificate", + type: "error" + }); + } + } + }; + return (
@@ -84,6 +126,11 @@ export const CertificatesSection = () => { + diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index 9bccff31d9..a601427629 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -68,6 +68,7 @@ type Props = { "deleteCertificate", "revokeCertificate", "certificateCert", + "certificateExport", "manageRenewal", "renewCertificate", "managePkiSyncs" @@ -275,7 +276,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} onClick={async () => - handlePopUpOpen("certificateCert", { + handlePopUpOpen("certificateExport", { serialNumber: certificate.serialNumber }) } diff --git a/frontend/src/views/PkiAlertsV2Page/components/CreatePkiAlertV2FormSteps.tsx b/frontend/src/views/PkiAlertsV2Page/components/CreatePkiAlertV2FormSteps.tsx index 2b5d0109d7..9dff01a315 100644 --- a/frontend/src/views/PkiAlertsV2Page/components/CreatePkiAlertV2FormSteps.tsx +++ b/frontend/src/views/PkiAlertsV2Page/components/CreatePkiAlertV2FormSteps.tsx @@ -188,7 +188,7 @@ export const CreatePkiAlertV2FormSteps = () => { name="name" render={({ field, fieldState: { error } }) => ( - + )} />