diff --git a/backend/package-lock.json b/backend/package-lock.json index 6a7920caf7c..504080a56c9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@ai-sdk/anthropic": "^3.0.68", + "@aws-sdk/client-acm": "^3.1030.0", "@aws-sdk/client-acm-pca": "^3.992.0", "@aws-sdk/client-elasticache": "^3.637.0", "@aws-sdk/client-iam": "^3.525.0", @@ -521,6 +522,57 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-acm": { + "version": "3.1031.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-acm/-/client-acm-3.1031.0.tgz", + "integrity": "sha512-QJxeg+GsXlAVdgiNgUb0pSUhoLP4x8S4VPFLGmimJx212/kfbNwBH+T7r5os09KR8U5gBAe2U8VLJhyF8dUl7w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/credential-provider-node": "^3.972.31", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.30", + "@aws-sdk/region-config-resolver": "^3.972.12", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.7", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.16", + "@smithy/config-resolver": "^4.4.16", + "@smithy/core": "^3.23.15", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.30", + "@smithy/middleware-retry": "^4.5.3", + "@smithy/middleware-serde": "^4.2.18", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.47", + "@smithy/util-defaults-mode-node": "^4.2.52", + "@smithy/util-endpoints": "^3.4.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.2", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.16", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-acm-pca": { "version": "3.992.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-acm-pca/-/client-acm-pca-3.992.0.tgz", @@ -1064,22 +1116,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.26.tgz", - "integrity": "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==", + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.0.tgz", + "integrity": "sha512-8j+dMtyDqNXFmi09CBdz8TY6Ltf2jhfHuP6ZvG4zVjndRc6JF0aeBUbRwQLndbptFCsdctRQgdNWecy4TIfXAw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.16", - "@smithy/core": "^3.23.13", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.18", + "@smithy/core": "^3.23.15", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.14", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1101,15 +1153,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.24.tgz", - "integrity": "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.26.tgz", + "integrity": "sha512-WBHAMxyPdgeJY6ZGLvq9mJwzZ+GaNUROQbfdVshtMsDVBrZTj5ZuFjKclSjSHvKSHJ4Y4O2yvI/aA/hrJbYfng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1117,20 +1169,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.26.tgz", - "integrity": "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==", + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.28.tgz", + "integrity": "sha512-+1DwCjjpo1WoiZTN08yGitI3nUwZUSQWVWFrW4C46HqZwACjcUQ7C66tnKPBTVxrEYYDOP11A6Afmu1L6ylt3g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.21", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" }, "engines": { @@ -1138,24 +1190,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.27.tgz", - "integrity": "sha512-Um26EsNSUfVUX0wUXnUA1W3wzKhVy6nviEElsh5lLZUYj9bk6DXOPnpte0gt+WHubcVfVsRk40bbm4KaroTEag==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.30.tgz", + "integrity": "sha512-Fg1oJcoijwOZjTxdbx+ubqbQl8YEQ4Cwhjw6TWzQjuDEvQYNhnCXW2pN7eKtdTrdE4a6+5TVKGSm2I+i2BKIQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/credential-provider-env": "^3.972.24", - "@aws-sdk/credential-provider-http": "^3.972.26", - "@aws-sdk/credential-provider-login": "^3.972.27", - "@aws-sdk/credential-provider-process": "^3.972.24", - "@aws-sdk/credential-provider-sso": "^3.972.27", - "@aws-sdk/credential-provider-web-identity": "^3.972.27", - "@aws-sdk/nested-clients": "^3.996.17", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/credential-provider-env": "^3.972.26", + "@aws-sdk/credential-provider-http": "^3.972.28", + "@aws-sdk/credential-provider-login": "^3.972.30", + "@aws-sdk/credential-provider-process": "^3.972.26", + "@aws-sdk/credential-provider-sso": "^3.972.30", + "@aws-sdk/credential-provider-web-identity": "^3.972.30", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1163,18 +1215,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.27.tgz", - "integrity": "sha512-t3ehEtHomGZwg5Gixw4fYbYtG9JBnjfAjSDabxhPEu/KLLUp0BB37/APX7MSKXQhX6ZH7pseuACFJ19NrAkNdg==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.30.tgz", + "integrity": "sha512-nchIrrI/7dgjG1bW/DEWOJc00K9n+kkl6B8Mk0KO6d4GfWBOXlVr9uHp7CJR9FIrjmov5SGjHXG2q9XAtkRw6Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.17", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1182,22 +1234,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.28.tgz", - "integrity": "sha512-rren+P6k5rShG5PX61iVi40kKdueyuMLBRTctQbyR5LooO9Ygr5L6R7ilG7RF1957NSH3KC3TU206fZuKwjSpQ==", + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.31.tgz", + "integrity": "sha512-99OHVQ6eZ5DTxiOWgHdjBMvLqv7xoY4jLK6nZ1NcNSQbAnYZkQNIHi/VqInc9fnmg7of9si/z+waE6YL9OQIlw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.24", - "@aws-sdk/credential-provider-http": "^3.972.26", - "@aws-sdk/credential-provider-ini": "^3.972.27", - "@aws-sdk/credential-provider-process": "^3.972.24", - "@aws-sdk/credential-provider-sso": "^3.972.27", - "@aws-sdk/credential-provider-web-identity": "^3.972.27", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/credential-provider-env": "^3.972.26", + "@aws-sdk/credential-provider-http": "^3.972.28", + "@aws-sdk/credential-provider-ini": "^3.972.30", + "@aws-sdk/credential-provider-process": "^3.972.26", + "@aws-sdk/credential-provider-sso": "^3.972.30", + "@aws-sdk/credential-provider-web-identity": "^3.972.30", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1205,16 +1257,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.24.tgz", - "integrity": "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.26.tgz", + "integrity": "sha512-jibxNld3m+vbmQwn98hcQ+fLIVrx3cQuhZlSs1/hix48SjDS5/pjMLwpmtLD/lFnd6ve1AL4o1bZg3X1WRa2SQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1222,18 +1274,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.27.tgz", - "integrity": "sha512-CWXeGjlbBuHcm9appZUgXKP2zHDyTti0/+gXpSFJ2J3CnSwf1KWjicjN0qG2ozkMH6blrrzMrimeIOEYNl238Q==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.30.tgz", + "integrity": "sha512-honYIM17F/+QSWJRE84T4u//ofqEi7rLbnwmIpu7fgFX5PML78wbtdSAy5Xwyve3TLpE9/f9zQx0aBVxSjAOPw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.17", - "@aws-sdk/token-providers": "3.1020.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/token-providers": "3.1031.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1241,17 +1293,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.27.tgz", - "integrity": "sha512-CUY4hQIFswdQNEsRGEzGBUKGMK5KpqmNDdu2ROMgI+45PLFS8H0y3Tm7kvM16uvvw3n1pVxk85tnRVUTgtaa1w==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.30.tgz", + "integrity": "sha512-CyL4oWUlONQRN2SsYMVrA9Z3i3QfLWTQctI8tuKbjNGCVVDCnJf/yMbSJCOZgpPFRtxh7dgQwvpqwmJm+iytmw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.17", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1335,14 +1387,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1364,13 +1416,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1378,15 +1430,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz", - "integrity": "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1447,18 +1499,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.27.tgz", - "integrity": "sha512-TIRLO5UR2+FVUGmhYoAwVkKhcVzywEDX/5LzR9tjy1h8FQAXOtFg2IqgmwvxU7y933rkTn9rl6AdgcAUgQ1/Kg==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.30.tgz", + "integrity": "sha512-lCz6JfelhjD6Eco1urXM2rOYRaxROSqeoY6IEKx+soegFJOajmIBCMHTAWuJl25Wf9IAST+i0/yOk9G3rMV26A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.13", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-retry": "^4.2.12", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.7", + "@smithy/core": "^3.23.15", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -1466,47 +1518,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.17.tgz", - "integrity": "sha512-7B0HIX0tEFmOSJuWzdHZj1WhMXSryM+h66h96ZkqSncoY7J6wq61KOu4Kr57b/YnJP3J/EeQYVFulgR281h+7A==", + "version": "3.996.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.20.tgz", + "integrity": "sha512-bzPdsNQnCh6TvvUmTHLZlL8qgyME6mNiUErcRMyJPywIl1BEu2VZRShel3mUoSh89bOBEXEWtjocDMolFxd/9A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.27", - "@aws-sdk/region-config-resolver": "^3.972.10", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.13", - "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.13", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/middleware-retry": "^4.4.45", - "@smithy/middleware-serde": "^4.2.16", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.30", + "@aws-sdk/region-config-resolver": "^3.972.12", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.7", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.16", + "@smithy/config-resolver": "^4.4.16", + "@smithy/core": "^3.23.15", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.30", + "@smithy/middleware-retry": "^4.5.3", + "@smithy/middleware-serde": "^4.2.18", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.44", - "@smithy/util-defaults-mode-node": "^4.2.48", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", + "@smithy/util-defaults-mode-browser": "^4.3.47", + "@smithy/util-defaults-mode-node": "^4.2.52", + "@smithy/util-endpoints": "^3.4.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1515,15 +1567,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.10.tgz", - "integrity": "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.12.tgz", + "integrity": "sha512-QQI43Mxd53nBij0pm8HXC+t4IOC6gnhhZfzxE0OATQyO6QfPV4e+aTIRRuAJKA6Nig/cR8eLwPryqYTX9ZrjAQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.13", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.16", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1548,17 +1600,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1020.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1020.0.tgz", - "integrity": "sha512-T61KA/VKl0zVUubdxigr1ut7SEpwE1/4CIKb14JDLyTAOne2yWKtQE1dDCSHl0UqrZNwW/bTt+EBHfQbslZJdw==", + "version": "3.1031.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1031.0.tgz", + "integrity": "sha512-zj/PvnbQK/2KJNln5K2QRI9HSsy+B4emz2gbQyUHkk6l7Lidu83P/9tfmC2cJXkcC3vdmyKH2DP3Iw/FDfKQuQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.17", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1566,12 +1618,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", - "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1591,15 +1643,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "version": "3.996.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.7.tgz", + "integrity": "sha512-ty4LQxN1QC+YhUP28NfEgZDEGXkyqOQy+BDriBozqHsrYO4JMgiPhfizqOGF7P+euBTZ5Ez6SKlLAMCLo8tzmw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.1", "tslib": "^2.6.2" }, "engines": { @@ -1618,27 +1670,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", - "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.13.tgz", - "integrity": "sha512-s1dCJ0J9WU9UPkT3FFqhKTSquYTkqWXGRaapHFyWwwJH86ZussewhNST5R5TwXVL1VSHq4aJVl9fWK+svaRVCQ==", + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.16.tgz", + "integrity": "sha512-ccvu0FNCI0C6OqmxI/tWn7BD8qGooWuURssiIM+6vbksFO8opXR4JOGtGYPj8QYzN/vfwNYrcK344PPbYuvzRg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.27", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/middleware-user-agent": "^3.972.30", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -1655,12 +1707,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", - "integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.18.tgz", + "integrity": "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, @@ -2278,6 +2330,7 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4336,6 +4389,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -4357,6 +4411,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -6941,6 +6996,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -7847,6 +7903,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -8402,6 +8459,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -9071,6 +9129,7 @@ "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.0.tgz", "integrity": "sha512-9GCWmVmKUAoRfloboCd+RKm6X17xn7eGL7HnpAZUnjBXBilWCxsKnLMTC/ixSHDKS/A/057M1Tx6ZUXd89sVBw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } @@ -9080,6 +9139,7 @@ "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9092,6 +9152,7 @@ "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.0.tgz", "integrity": "sha512-eIrPW9PIFgDopQU0e/OPpwCW2QWQDtNZDSsiN4sJO8KdMnWWnXJicnRfzrit5rHwFo+Y98i+w/Y5ScnBAFr1dQ==", "license": "MIT", + "peer": true, "dependencies": { "prismjs": "^1.30.0" }, @@ -9107,6 +9168,7 @@ "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz", "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9182,6 +9244,7 @@ "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz", "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9215,6 +9278,7 @@ "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz", "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9227,6 +9291,7 @@ "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz", "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9251,6 +9316,7 @@ "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz", "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9263,6 +9329,7 @@ "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz", "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9290,6 +9357,7 @@ "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz", "integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9862,6 +9930,7 @@ "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10481,16 +10550,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", - "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.16.tgz", + "integrity": "sha512-GFlGPNLZKrGfqWpqVb31z7hvYCA9ZscfX1buYnvvMGcRYsQQnhH+4uN6mWWflcD5jB4OXP/LBrdpukEdjl41tg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-endpoints": "^3.4.1", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -10498,18 +10567,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.13", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.13.tgz", - "integrity": "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==", + "version": "3.23.15", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.15.tgz", + "integrity": "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.21", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -10519,15 +10588,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", - "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -10605,14 +10674,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", - "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -10636,12 +10705,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", - "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -10665,12 +10734,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", - "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10704,13 +10773,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", - "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10718,18 +10787,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.28", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.28.tgz", - "integrity": "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==", + "version": "4.4.30", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.30.tgz", + "integrity": "sha512-qS2XqhKeXmdZ4nEQ4cOxIczSP/Y91wPAHYuRwmWDCh975B7/57uxsm5d6sisnUThn2u2FwzMdJNM7AbO1YPsPg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/middleware-serde": "^4.2.16", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-middleware": "^4.2.12", + "@smithy/core": "^3.23.15", + "@smithy/middleware-serde": "^4.2.18", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -10737,18 +10806,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.46", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.46.tgz", - "integrity": "sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.3.tgz", + "integrity": "sha512-TE8dJNi6JuxzGSxMCVd3i9IEWDndCl3bmluLsBNDWok8olgj65OfkndMhl9SZ7m14c+C5SQn/PcUmrDl57rSFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.13", + "@smithy/core": "^3.23.15", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.2.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -10757,14 +10827,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.16.tgz", - "integrity": "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==", + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.18.tgz", + "integrity": "sha512-M6CSgnp3v4tYz9ynj2JHbA60woBZcGqEwNjTKjBsNHPV26R1ZX52+0wW8WsZU18q45jD0tw2wL22S17Ze9LpEw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.23.15", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10772,12 +10842,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", - "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10785,14 +10855,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", - "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10800,14 +10870,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.1.tgz", - "integrity": "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.3.tgz", + "integrity": "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10815,12 +10885,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", - "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10828,12 +10898,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", - "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10841,12 +10911,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", - "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -10855,12 +10925,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", - "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10868,24 +10938,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", - "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.14.tgz", + "integrity": "sha512-vVimoUnGxlx4eLLQbZImdOZFOe+Zh+5ACntv8VxZuGP72LdWu5GV3oEmCahSEReBgRJoWjypFkrehSj7BWx1HQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1" + "@smithy/types": "^4.14.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", - "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10893,16 +10963,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", - "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.14", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -10912,17 +10982,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.8", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.8.tgz", - "integrity": "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==", + "version": "4.12.11", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.11.tgz", + "integrity": "sha512-wzz/Wa1CH/Tlhxh0s4DQPEcXSxSVfJ59AZcUh9Gu0c6JTlKuwGf4o/3P2TExv0VbtPFt8odIBG+eQGK2+vTECg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.21", + "@smithy/core": "^3.23.15", + "@smithy/middleware-endpoint": "^4.4.30", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" }, "engines": { @@ -10930,9 +11000,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10942,13 +11012,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", - "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11019,14 +11089,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.44", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.44.tgz", - "integrity": "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==", + "version": "4.3.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.47.tgz", + "integrity": "sha512-zlIuXai3/SHjQUQ8y3g/woLvrH573SK2wNjcDaHu5e9VOcC0JwM1MI0Sq0GZJyN3BwSUneIhpjZ18nsiz5AtQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11034,17 +11104,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.48", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.48.tgz", - "integrity": "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==", + "version": "4.2.52", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.52.tgz", + "integrity": "sha512-cQBz8g68Vnw1W2meXlkb3D/hXJU+Taiyj9P8qLJtjREEV9/Td65xi4A/H1sRQ8EIgX5qbZbvdYPKygKLholZ3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.13", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@smithy/config-resolver": "^4.4.16", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11052,13 +11122,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", - "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.1.tgz", + "integrity": "sha512-wMxNDZJrgS5mQV9oxCs4TWl5767VMgOfqfZ3JHyCkMtGC2ykW9iPqMvFur695Otcc5yxLG8OKO/80tsQBxrhXg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11078,12 +11148,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", - "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11091,13 +11161,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.13.tgz", - "integrity": "sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.2.tgz", + "integrity": "sha512-2+KTsJEwTi63NUv4uR9IQ+IFT1yu6Rf6JuoBK2WKaaJ/TRvOiOVGcXAsEqX/TQN2thR9yII21kPUJq1UV/WI2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/service-error-classification": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11105,14 +11175,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.21", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.21.tgz", - "integrity": "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==", + "version": "4.5.23", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.23.tgz", + "integrity": "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/types": "^4.13.1", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -11149,12 +11219,12 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.14.tgz", - "integrity": "sha512-2zqq5o/oizvMaFUlNiTyZ7dbgYv1a893aGut2uaxtbzTx/VYYnRxWzDHuD/ftgcw94ffenua+ZNLrbqwUYE+Bg==", + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.16.tgz", + "integrity": "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11516,6 +11586,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -11974,6 +12045,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -12420,6 +12492,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -12933,6 +13006,7 @@ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "license": "MIT", + "peer": true, "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -13145,6 +13219,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -13776,6 +13851,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -15272,6 +15348,7 @@ "version": "16.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "peer": true, "engines": { "node": ">=12" }, @@ -15642,6 +15719,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -15724,6 +15802,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -15822,6 +15901,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -15910,6 +15990,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -16322,6 +16403,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -16395,7 +16477,6 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "~0.7.2", "cookie-signature": "~1.0.7", @@ -16417,14 +16498,12 @@ "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "peer": true + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" }, "node_modules/express-session/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -16432,8 +16511,7 @@ "node_modules/express-session/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/cookie-signature": { "version": "1.0.6", @@ -17929,6 +18007,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -21331,7 +21410,6 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -22019,6 +22097,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.2.tgz", "integrity": "sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -22361,6 +22440,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -22458,6 +22538,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -22904,7 +22985,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -23008,6 +23088,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -23017,6 +23098,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -24756,6 +24838,7 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -26112,6 +26195,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -26280,6 +26364,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26320,7 +26405,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "peer": true, "dependencies": { "random-bytes": "~1.0.0" }, @@ -26623,6 +26707,7 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -28035,6 +28120,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/backend/package.json b/backend/package.json index f0379c15be1..c4ffacc9e93 100644 --- a/backend/package.json +++ b/backend/package.json @@ -140,6 +140,7 @@ }, "dependencies": { "@ai-sdk/anthropic": "^3.0.68", + "@aws-sdk/client-acm": "^3.1030.0", "@aws-sdk/client-acm-pca": "^3.992.0", "@aws-sdk/client-elasticache": "^3.637.0", "@aws-sdk/client-iam": "^3.525.0", diff --git a/backend/src/db/migrations/20260416231234_add-external-metadata-to-certificates.ts b/backend/src/db/migrations/20260416231234_add-external-metadata-to-certificates.ts new file mode 100644 index 00000000000..568d30f90d0 --- /dev/null +++ b/backend/src/db/migrations/20260416231234_add-external-metadata-to-certificates.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Certificate)) { + const hasColumn = await knex.schema.hasColumn(TableName.Certificate, "externalMetadata"); + if (!hasColumn) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.jsonb("externalMetadata").nullable(); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Certificate)) { + if (await knex.schema.hasColumn(TableName.Certificate, "externalMetadata")) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.dropColumn("externalMetadata"); + }); + } + } +} diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 68325058faf..b69adb06017 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -44,7 +44,8 @@ export const CertificatesSchema = z.object({ isCA: z.boolean().nullable().optional(), pathLength: z.number().nullable().optional(), source: z.string().nullable().optional(), - discoveryMetadata: z.unknown().nullable().optional() + discoveryMetadata: z.unknown().nullable().optional(), + externalMetadata: z.unknown().nullable().optional() }); export type TCertificates = z.infer; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 50a771fab0c..700fc13811c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2555,6 +2555,12 @@ export const CertificateAuthorities = { certificateAuthorityArn: `The ARN of the AWS Private Certificate Authority to use for issuing certificates.`, region: `The AWS region where the Private Certificate Authority is located.` }, + AWS_ACM_PUBLIC_CA: { + appConnectionId: `The ID of the AWS App Connection to use for authenticating with AWS Certificate Manager (ACM). This connection must have permissions to request, describe, export, renew, and delete certificates.`, + dnsAppConnectionId: `The ID of the AWS App Connection to use for creating and managing Route 53 CNAME records required for ACM domain validation.`, + hostedZoneId: `The Route 53 hosted zone ID to use for ACM DNS validation CNAME records.`, + region: `The AWS region to use for the ACM API calls.` + }, INTERNAL: { type: "The type of CA to create.", friendlyName: "A friendly name for the CA.", diff --git a/backend/src/server/routes/v1/certificate-authority-routers/aws-acm-public-ca-certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-routers/aws-acm-public-ca-certificate-authority-router.ts new file mode 100644 index 00000000000..d916752b210 --- /dev/null +++ b/backend/src/server/routes/v1/certificate-authority-routers/aws-acm-public-ca-certificate-authority-router.ts @@ -0,0 +1,18 @@ +import { + AwsAcmPublicCaCertificateAuthoritySchema, + CreateAwsAcmPublicCaCertificateAuthoritySchema, + UpdateAwsAcmPublicCaCertificateAuthoritySchema +} from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints"; + +export const registerAwsAcmPublicCaCertificateAuthorityRouter = async (server: FastifyZodProvider) => { + registerCertificateAuthorityEndpoints({ + caType: CaType.AWS_ACM_PUBLIC_CA, + server, + responseSchema: AwsAcmPublicCaCertificateAuthoritySchema, + createSchema: CreateAwsAcmPublicCaCertificateAuthoritySchema, + updateSchema: UpdateAwsAcmPublicCaCertificateAuthoritySchema + }); +}; diff --git a/backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts index 81f36c9e7a6..bcab60f57ed 100644 --- a/backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts @@ -6,6 +6,7 @@ import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas"; +import { AwsAcmPublicCaCertificateAuthoritySchema } from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas"; import { AwsPcaCertificateAuthoritySchema } from "@app/services/certificate-authority/aws-pca/aws-pca-certificate-authority-schemas"; import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; @@ -15,7 +16,8 @@ const CertificateAuthoritySchema = z.discriminatedUnion("type", [ InternalCertificateAuthoritySchema, AcmeCertificateAuthoritySchema, AzureAdCsCertificateAuthoritySchema, - AwsPcaCertificateAuthoritySchema + AwsPcaCertificateAuthoritySchema, + AwsAcmPublicCaCertificateAuthoritySchema ]); export const registerGeneralCertificateAuthorityRouter = async (server: FastifyZodProvider) => { @@ -73,6 +75,14 @@ export const registerGeneralCertificateAuthorityRouter = async (server: FastifyZ req.permission ); + const awsAcmPublicCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { + projectId: req.query.projectId, + type: CaType.AWS_ACM_PUBLIC_CA + }, + req.permission + ); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: req.query.projectId, @@ -83,7 +93,8 @@ export const registerGeneralCertificateAuthorityRouter = async (server: FastifyZ ...(internalCas ?? []).map((ca) => ca.id), ...(acmeCas ?? []).map((ca) => ca.id), ...(azureAdCsCas ?? []).map((ca) => ca.id), - ...(awsPcaCas ?? []).map((ca) => ca.id) + ...(awsPcaCas ?? []).map((ca) => ca.id), + ...(awsAcmPublicCas ?? []).map((ca) => ca.id) ] } } @@ -94,7 +105,8 @@ export const registerGeneralCertificateAuthorityRouter = async (server: FastifyZ ...(internalCas ?? []), ...(acmeCas ?? []), ...(azureAdCsCas ?? []), - ...(awsPcaCas ?? []) + ...(awsPcaCas ?? []), + ...(awsAcmPublicCas ?? []) ] }; } diff --git a/backend/src/server/routes/v1/certificate-authority-routers/index.ts b/backend/src/server/routes/v1/certificate-authority-routers/index.ts index 5b87322ac52..48ff71b4f53 100644 --- a/backend/src/server/routes/v1/certificate-authority-routers/index.ts +++ b/backend/src/server/routes/v1/certificate-authority-routers/index.ts @@ -1,6 +1,7 @@ import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router"; +import { registerAwsAcmPublicCaCertificateAuthorityRouter } from "./aws-acm-public-ca-certificate-authority-router"; import { registerAwsPcaCertificateAuthorityRouter } from "./aws-pca-certificate-authority-router"; import { registerAzureAdCsCertificateAuthorityRouter } from "./azure-ad-cs-certificate-authority-router"; import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router"; @@ -12,5 +13,6 @@ export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record { @@ -73,6 +75,14 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { req.permission ); + const awsAcmPublicCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { + projectId: req.query.projectId, + type: CaType.AWS_ACM_PUBLIC_CA + }, + req.permission + ); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: req.query.projectId, @@ -83,7 +93,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { ...(internalCas ?? []).map((ca) => ca.id), ...(acmeCas ?? []).map((ca) => ca.id), ...(azureAdCsCas ?? []).map((ca) => ca.id), - ...(awsPcaCas ?? []).map((ca) => ca.id) + ...(awsPcaCas ?? []).map((ca) => ca.id), + ...(awsAcmPublicCas ?? []).map((ca) => ca.id) ] } } @@ -94,7 +105,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { ...(internalCas ?? []), ...(acmeCas ?? []), ...(azureAdCsCas ?? []), - ...(awsPcaCas ?? []) + ...(awsPcaCas ?? []), + ...(awsAcmPublicCas ?? []) ] }; } diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 05047274eef..3ddb3101942 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -43,6 +43,7 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; import { CaStatus, CaType } from "../certificate-authority-enums"; import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; +import { route53DeleteRecord, route53UpsertRecord } from "../dns-providers/route53"; import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal"; import { AcmeDnsProvider } from "./acme-certificate-authority-enums"; import { AcmeCertificateAuthorityCredentialsSchema } from "./acme-certificate-authority-schemas"; @@ -54,7 +55,6 @@ import { import { azureDnsDeleteTxtRecord, azureDnsInsertTxtRecord } from "./dns-providers/azure-dns"; import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare"; import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy"; -import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54"; const validateDnsResolver = (resolver: string): void => { const appCfg = getConfig(); @@ -422,12 +422,13 @@ export const orderCertificate = async ( switch (acmeCa.configuration.dnsProviderConfig.provider) { case AcmeDnsProvider.Route53: { - await route53InsertTxtRecord( - connection as TAwsConnection, - acmeCa.configuration.dnsProviderConfig.hostedZoneId, - recordName, - recordValue - ); + await route53UpsertRecord(connection as TAwsConnection, acmeCa.configuration.dnsProviderConfig.hostedZoneId, { + name: recordName, + type: "TXT", + value: recordValue, + ttl: 30, + comment: "Set ACME challenge TXT record" + }); break; } case AcmeDnsProvider.Cloudflare: { @@ -478,12 +479,13 @@ export const orderCertificate = async ( switch (acmeCa.configuration.dnsProviderConfig.provider) { case AcmeDnsProvider.Route53: { - await route53DeleteTxtRecord( - connection as TAwsConnection, - acmeCa.configuration.dnsProviderConfig.hostedZoneId, - recordName, - recordValue - ); + await route53DeleteRecord(connection as TAwsConnection, acmeCa.configuration.dnsProviderConfig.hostedZoneId, { + name: recordName, + type: "TXT", + value: recordValue, + ttl: 30, + comment: "Delete ACME challenge TXT record" + }); break; } case AcmeDnsProvider.Cloudflare: { diff --git a/backend/src/services/certificate-authority/acme/dns-providers/route54.ts b/backend/src/services/certificate-authority/acme/dns-providers/route54.ts deleted file mode 100644 index e2878ad4835..00000000000 --- a/backend/src/services/certificate-authority/acme/dns-providers/route54.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53"; - -import { CustomAWSHasher } from "@app/lib/aws/hashing"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; -import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; -import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types"; - -export const route53InsertTxtRecord = async ( - connection: TAwsConnectionConfig, - hostedZoneId: string, - domain: string, - value: string -) => { - const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global - const route53Client = new Route53Client({ - sha256: CustomAWSHasher, - useFipsEndpoint: crypto.isFipsModeEnabled(), - credentials: config.credentials, - region: config.region - }); - - const command = new ChangeResourceRecordSetsCommand({ - HostedZoneId: hostedZoneId, - ChangeBatch: { - Comment: "Set ACME challenge TXT record", - Changes: [ - { - Action: "UPSERT", - ResourceRecordSet: { - Name: domain, - Type: "TXT", - TTL: 30, - ResourceRecords: [{ Value: value }] - } - } - ] - } - }); - - await route53Client.send(command); -}; - -export const route53DeleteTxtRecord = async ( - connection: TAwsConnectionConfig, - hostedZoneId: string, - domain: string, - value: string -) => { - const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global - const route53Client = new Route53Client({ - credentials: config.credentials, - region: config.region - }); - - const command = new ChangeResourceRecordSetsCommand({ - HostedZoneId: hostedZoneId, - ChangeBatch: { - Comment: "Delete ACME challenge TXT record", - Changes: [ - { - Action: "DELETE", - ResourceRecordSet: { - Name: domain, - Type: "TXT", - TTL: 30, - ResourceRecords: [{ Value: value }] - } - } - ] - } - }); - - await route53Client.send(command); -}; diff --git a/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-client.ts b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-client.ts new file mode 100644 index 00000000000..c4c624cfb47 --- /dev/null +++ b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-client.ts @@ -0,0 +1,60 @@ +import { ACMClient } from "@aws-sdk/client-acm"; + +import { CustomAWSHasher } from "@app/lib/aws/hashing"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { NotFoundError } from "@app/lib/errors"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +export const createAcmClient = async ({ + appConnectionId, + region, + appConnectionDAL, + kmsService +}: { + appConnectionId: string; + region: AWSRegion; + appConnectionDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" + >; +}) => { + const appConnection = await appConnectionDAL.findById(appConnectionId); + if (!appConnection) { + throw new NotFoundError({ message: `App connection with ID '${appConnectionId}' not found` }); + } + + const decryptedConnection = (await decryptAppConnection(appConnection, kmsService)) as TAwsConnection; + const awsConfig = await getAwsConnectionConfig(decryptedConnection, region); + + return new ACMClient({ + sha256: CustomAWSHasher, + useFipsEndpoint: crypto.isFipsModeEnabled(), + credentials: awsConfig.credentials, + region: awsConfig.region + }); +}; + +export const resolveDnsAwsConnection = async ({ + dnsAppConnectionId, + appConnectionDAL, + kmsService +}: { + dnsAppConnectionId: string; + appConnectionDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" + >; +}) => { + const dnsAppConnection = await appConnectionDAL.findById(dnsAppConnectionId); + if (!dnsAppConnection) { + throw new NotFoundError({ message: `DNS app connection with ID '${dnsAppConnectionId}' not found` }); + } + return (await decryptAppConnection(dnsAppConnection, kmsService)) as TAwsConnection; +}; diff --git a/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-enums.ts b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-enums.ts new file mode 100644 index 00000000000..c2a669d21e7 --- /dev/null +++ b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-enums.ts @@ -0,0 +1,9 @@ +export enum AwsAcmValidationMethod { + DNS = "DNS" +} + +/** + * ACM public certificates have a fixed validity period (as of 2025). + * See: https://docs.aws.amazon.com/acm/latest/userguide/managed-renewal.html + */ +export const AWS_ACM_CERTIFICATE_VALIDITY_DAYS = 198; diff --git a/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-errors.ts b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-errors.ts new file mode 100644 index 00000000000..66bdf47f292 --- /dev/null +++ b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-errors.ts @@ -0,0 +1,15 @@ +/* eslint-disable max-classes-per-file */ + +export class AcmPendingError extends Error { + constructor(message: string) { + super(message); + this.name = "AcmPendingError"; + } +} + +export class AcmTerminalError extends Error { + constructor(message: string) { + super(message); + this.name = "AcmTerminalError"; + } +} diff --git a/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-fns.ts b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-fns.ts new file mode 100644 index 00000000000..d4a44c45300 --- /dev/null +++ b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-fns.ts @@ -0,0 +1,865 @@ +/* eslint-disable no-await-in-loop */ +import { + ACMClient, + CertificateExport, + CertificateStatus, + DescribeCertificateCommand, + ExportCertificateCommand, + ListCertificatesCommand, + RenewCertificateCommand, + RequestCertificateCommand, + RevocationReason, + RevokeCertificateCommand, + ValidationMethod +} from "@aws-sdk/client-acm"; +import * as x509 from "@peculiar/x509"; +import RE2 from "re2"; + +import { TableName } from "@app/db/schemas"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils"; +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { extractCertificateFields } from "@app/services/certificate/certificate-fns"; +import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; +import { + CertExtendedKeyUsage, + CertExtendedKeyUsageOIDToName, + CertKeyAlgorithm, + CertKeyUsage, + CertSignatureAlgorithm, + CertStatus, + CertSubjectAlternativeNameType, + CrlReason +} from "@app/services/certificate/certificate-types"; +import { ExternalMetadataSchema } from "@app/services/certificate-common/external-metadata-schemas"; +import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; + +import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; +import { CaStatus, CaType } from "../certificate-authority-enums"; +import { route53GetHostedZone, route53UpsertRecord } from "../dns-providers/route53"; +import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal"; +import { createAcmClient, resolveDnsAwsConnection } from "./aws-acm-public-ca-certificate-authority-client"; +import { AwsAcmValidationMethod } from "./aws-acm-public-ca-certificate-authority-enums"; +import { AcmPendingError, AcmTerminalError } from "./aws-acm-public-ca-certificate-authority-errors"; +import { + TAwsAcmPublicCaCertificateAuthority, + TCreateAwsAcmPublicCaCertificateAuthorityDTO, + TUpdateAwsAcmPublicCaCertificateAuthorityDTO +} from "./aws-acm-public-ca-certificate-authority-types"; +import { + buildIdempotencyToken, + generateAcmPassphrase, + mapCertKeyAlgorithmToAcm, + validateAcmIssuanceInputs +} from "./aws-acm-public-ca-certificate-authority-validators"; + +const CRL_REASON_TO_ACM_REVOCATION_REASON_MAP: Record = { + [CrlReason.UNSPECIFIED]: RevocationReason.UNSPECIFIED, + [CrlReason.KEY_COMPROMISE]: RevocationReason.KEY_COMPROMISE, + [CrlReason.CA_COMPROMISE]: RevocationReason.CA_COMPROMISE, + [CrlReason.AFFILIATION_CHANGED]: RevocationReason.AFFILIATION_CHANGED, + [CrlReason.SUPERSEDED]: RevocationReason.SUPERSEDED, + [CrlReason.CESSATION_OF_OPERATION]: RevocationReason.CESSATION_OF_OPERATION, + [CrlReason.CERTIFICATE_HOLD]: RevocationReason.CERTIFICATE_HOLD, + [CrlReason.PRIVILEGE_WITHDRAWN]: RevocationReason.PRIVILEGE_WITHDRAWN, + [CrlReason.A_A_COMPROMISE]: RevocationReason.A_A_COMPROMISE +}; + +type TAwsAcmPublicCaCertificateAuthorityFnsDeps = { + appConnectionDAL: Pick; + appConnectionService: Pick; + certificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById" + >; + externalCertificateAuthorityDAL: Pick; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" + >; + projectDAL: Pick; + certificateProfileDAL?: Pick; +}; + +export const castDbEntryToAwsAcmPublicCaCertificateAuthority = ( + ca: Awaited> +): TAwsAcmPublicCaCertificateAuthority => { + if (!ca.externalCa?.id) { + throw new BadRequestError({ message: "Malformed AWS ACM Public Certificate Authority" }); + } + + if (!ca.externalCa.appConnectionId) { + throw new BadRequestError({ + message: "AWS app connection ID is missing from certificate authority configuration" + }); + } + + const configuration = ca.externalCa.configuration as { + dnsAppConnectionId?: string; + hostedZoneId?: string; + region: AWSRegion; + }; + + if (!configuration.region || !configuration.dnsAppConnectionId || !configuration.hostedZoneId) { + throw new BadRequestError({ + message: "AWS ACM configuration is incomplete — region, Route 53 connection, and hosted zone ID are required" + }); + } + + return { + id: ca.id, + type: CaType.AWS_ACM_PUBLIC_CA, + enableDirectIssuance: ca.enableDirectIssuance, + name: ca.name, + projectId: ca.projectId, + configuration: { + appConnectionId: ca.externalCa.appConnectionId, + dnsAppConnectionId: configuration.dnsAppConnectionId, + hostedZoneId: configuration.hostedZoneId, + region: configuration.region + }, + status: ca.status as CaStatus + }; +}; + +export const AwsAcmPublicCaCertificateAuthorityFns = ({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + certificateProfileDAL +}: TAwsAcmPublicCaCertificateAuthorityFnsDeps) => { + const validateAwsConnection = async ({ + appConnectionId, + dnsAppConnectionId, + projectId, + actor + }: { + appConnectionId: string; + dnsAppConnectionId?: string; + projectId: string; + actor: OrgServiceActor; + }) => { + const appConnection = await appConnectionDAL.findById(appConnectionId); + if (!appConnection) { + throw new NotFoundError({ message: `App connection with ID '${appConnectionId}' not found` }); + } + if (appConnection.app !== AppConnection.AWS) { + throw new BadRequestError({ + message: `App connection with ID '${appConnectionId}' is not an AWS connection` + }); + } + await appConnectionService.validateAppConnectionUsageById( + appConnection.app as AppConnection, + { connectionId: appConnectionId, projectId }, + actor + ); + + if (dnsAppConnectionId && dnsAppConnectionId !== appConnectionId) { + const dnsAppConnection = await appConnectionDAL.findById(dnsAppConnectionId); + if (!dnsAppConnection) { + throw new NotFoundError({ message: `DNS app connection with ID '${dnsAppConnectionId}' not found` }); + } + if (dnsAppConnection.app !== AppConnection.AWS) { + throw new BadRequestError({ + message: `DNS app connection with ID '${dnsAppConnectionId}' is not an AWS connection` + }); + } + await appConnectionService.validateAppConnectionUsageById( + dnsAppConnection.app as AppConnection, + { connectionId: dnsAppConnectionId, projectId }, + actor + ); + } + }; + + const createCertificateAuthority = async ({ + name, + projectId, + configuration, + actor, + status + }: { + status: CaStatus; + name: string; + projectId: string; + configuration: TCreateAwsAcmPublicCaCertificateAuthorityDTO["configuration"]; + actor: OrgServiceActor; + }) => { + const { appConnectionId, dnsAppConnectionId, hostedZoneId, region } = configuration; + + await validateAwsConnection({ appConnectionId, dnsAppConnectionId, projectId, actor }); + + // Smoke-test both connections up front — ACM via ListCertificates (no single "get CA" resource), + // and Route 53 via GetHostedZone so a misconfigured DNS connection / wrong zone ID fails + // synchronously here instead of mid-issuance. + const acmClient = await createAcmClient({ appConnectionId, region, appConnectionDAL, kmsService }); + try { + await acmClient.send(new ListCertificatesCommand({ MaxItems: 1 })); + } catch (error) { + throw new BadRequestError({ + message: `Failed to reach AWS Certificate Manager: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + + const dnsConnection = await resolveDnsAwsConnection({ dnsAppConnectionId, appConnectionDAL, kmsService }); + try { + await route53GetHostedZone(dnsConnection, hostedZoneId); + } catch (error) { + throw new BadRequestError({ + message: `Failed to access Route 53 hosted zone: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + + const caEntity = await certificateAuthorityDAL.transaction(async (tx) => { + try { + const ca = await certificateAuthorityDAL.create( + { + projectId, + enableDirectIssuance: false, + name, + status + }, + tx + ); + + await externalCertificateAuthorityDAL.create( + { + caId: ca.id, + appConnectionId, + type: CaType.AWS_ACM_PUBLIC_CA, + configuration: { + dnsAppConnectionId, + hostedZoneId, + region + } + }, + tx + ); + + return await certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + if ((error as any)?.error?.code === "23505") { + throw new BadRequestError({ + message: "Certificate authority with the same name already exists in your project" + }); + } + throw error; + } + }); + + if (!caEntity.externalCa?.id) { + throw new BadRequestError({ message: "Failed to create external certificate authority" }); + } + + return castDbEntryToAwsAcmPublicCaCertificateAuthority(caEntity); + }; + + const updateCertificateAuthority = async ({ + id, + status, + configuration, + actor, + name + }: { + id: string; + status?: CaStatus; + configuration: TUpdateAwsAcmPublicCaCertificateAuthorityDTO["configuration"]; + actor: OrgServiceActor; + name?: string; + }) => { + if (configuration) { + const { appConnectionId, dnsAppConnectionId, hostedZoneId, region } = configuration; + + const ca = await certificateAuthorityDAL.findById(id); + if (!ca) { + throw new NotFoundError({ message: `Could not find Certificate Authority with ID "${id}"` }); + } + + await validateAwsConnection({ appConnectionId, dnsAppConnectionId, projectId: ca.projectId, actor }); + + const acmClient = await createAcmClient({ appConnectionId, region, appConnectionDAL, kmsService }); + try { + await acmClient.send(new ListCertificatesCommand({ MaxItems: 1 })); + } catch (error) { + throw new BadRequestError({ + message: `Failed to reach AWS Certificate Manager: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + + const dnsConnection = await resolveDnsAwsConnection({ dnsAppConnectionId, appConnectionDAL, kmsService }); + try { + await route53GetHostedZone(dnsConnection, hostedZoneId); + } catch (error) { + throw new BadRequestError({ + message: `Failed to access Route 53 hosted zone: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + } + + const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { + if (configuration) { + await externalCertificateAuthorityDAL.update( + { + caId: id, + type: CaType.AWS_ACM_PUBLIC_CA + }, + { + appConnectionId: configuration.appConnectionId, + configuration: { + dnsAppConnectionId: configuration.dnsAppConnectionId, + hostedZoneId: configuration.hostedZoneId, + region: configuration.region + } + }, + tx + ); + } + + if (name || status) { + await certificateAuthorityDAL.updateById( + id, + { + name, + status + }, + tx + ); + } + + return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx); + }); + + if (!updatedCa.externalCa?.id) { + throw new BadRequestError({ message: "Failed to update external certificate authority" }); + } + + return castDbEntryToAwsAcmPublicCaCertificateAuthority(updatedCa); + }; + + const listCertificateAuthorities = async ({ + projectId, + permissionFilters + }: { + projectId: string; + permissionFilters?: ProcessedPermissionRules; + }) => { + const cas = await certificateAuthorityDAL.findWithAssociatedCa( + { + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AWS_ACM_PUBLIC_CA + }, + {}, + permissionFilters + ); + + return cas.map(castDbEntryToAwsAcmPublicCaCertificateAuthority); + }; + + /** + * Issues (or renews) a certificate from AWS Certificate Manager. + * + * Idempotent via AWS's IdempotencyToken: retrying the same certificateId within + * AWS's 1-hour window returns the same certificate ARN, so we don't need to persist + * intermediate state across retries. The cert record is only created when everything + * completes, in a single DB transaction. If DNS validation is still pending, this + * function throws AcmPendingError and the queue retries. + */ + const orderCertificateFromProfile = async ({ + caId, + profileId, + commonName, + altNames = [], + keyAlgorithm = CertKeyAlgorithm.RSA_2048, + isRenewal, + originalCertificateId, + certificateId, + csr, + validity, + organization, + organizationalUnit, + country, + state, + locality, + keyUsages = [], + extendedKeyUsages = [] + }: { + caId: string; + profileId: string; + commonName: string; + altNames?: Array<{ type: CertSubjectAlternativeNameType; value: string }>; + keyAlgorithm?: CertKeyAlgorithm; + isRenewal?: boolean; + originalCertificateId?: string; + certificateId: string; + csr?: string; + validity?: { ttl?: string }; + organization?: string; + organizationalUnit?: string; + country?: string; + state?: string; + locality?: string; + keyUsages?: string[]; + extendedKeyUsages?: string[]; + }) => { + validateAcmIssuanceInputs({ + csr, + keyAlgorithm, + altNames, + ttl: validity?.ttl, + organization, + organizationalUnit, + country, + state, + locality, + isRenewal + }); + + if (keyUsages.length > 0 || extendedKeyUsages.length > 0) { + logger.info( + `[caId=${caId}] AWS ACM overrides caller-specified key usages and extended key usages with its own current policy.` + ); + } + + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca.externalCa || ca.externalCa.type !== CaType.AWS_ACM_PUBLIC_CA) { + throw new BadRequestError({ message: "CA is not an AWS ACM Public Certificate Authority" }); + } + + const acmCa = castDbEntryToAwsAcmPublicCaCertificateAuthority(ca); + if (acmCa.status !== CaStatus.ACTIVE) { + throw new BadRequestError({ message: "CA is disabled" }); + } + + const { appConnectionId, dnsAppConnectionId, hostedZoneId, region } = acmCa.configuration; + + // ACM ARNs are region-locked. On renewal this gets overwritten with the original + // cert's region so the stored metadata stays consistent with the ARN even if the + // CA's configured region was edited between issuance and renewal. + let issuanceRegion: AWSRegion = region; + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: certificateManagerKmsId }); + let certificateArn: string; + let acmClient: ACMClient; + + if (isRenewal && originalCertificateId) { + const originalCert = await certificateDAL.findById(originalCertificateId); + if (!originalCert) { + throw new BadRequestError({ message: `Original certificate ${originalCertificateId} not found` }); + } + const parsedMetadata = ExternalMetadataSchema.safeParse(originalCert.externalMetadata); + if ( + !parsedMetadata.success || + parsedMetadata.data.type !== CaType.AWS_ACM_PUBLIC_CA || + !parsedMetadata.data.arn + ) { + throw new BadRequestError({ + message: "Original certificate is missing AWS ACM metadata — cannot renew" + }); + } + certificateArn = parsedMetadata.data.arn; + issuanceRegion = parsedMetadata.data.region; + + acmClient = await createAcmClient({ + appConnectionId, + region: issuanceRegion, + appConnectionDAL, + kmsService + }); + + const describe = await acmClient.send(new DescribeCertificateCommand({ CertificateArn: certificateArn })); + const detail = describe.Certificate; + if (!detail) { + throw new BadRequestError({ message: `ACM did not return details for certificate ${certificateArn}` }); + } + // ACM serials may come back colon-separated hex (e.g., "0a:1b:..."), our DB stores plain hex — + // normalize both before comparing. + const normalizeSerial = (s?: string | null) => s?.split(":").join("").toLowerCase() ?? ""; + const storedSerial = normalizeSerial(originalCert.serialNumber); + const awsSerial = normalizeSerial(detail.Serial); + const alreadyRenewedByAws = Boolean(awsSerial && storedSerial && awsSerial !== storedSerial); + + if (!alreadyRenewedByAws) { + if (detail.DomainValidationOptions) { + const dnsConnection = await resolveDnsAwsConnection({ + dnsAppConnectionId, + appConnectionDAL, + kmsService + }); + for (const dv of detail.DomainValidationOptions) { + if (dv.ResourceRecord?.Name && dv.ResourceRecord?.Value) { + await route53UpsertRecord(dnsConnection, hostedZoneId, { + name: dv.ResourceRecord.Name, + type: "CNAME", + value: dv.ResourceRecord.Value + }); + } + } + } + + const renewalInProgress = detail.RenewalSummary?.RenewalStatus === "PENDING_VALIDATION"; + if (!renewalInProgress) { + await acmClient.send(new RenewCertificateCommand({ CertificateArn: certificateArn })); + } + + const afterRenew = await acmClient.send(new DescribeCertificateCommand({ CertificateArn: certificateArn })); + const renewStatus = afterRenew.Certificate?.RenewalSummary?.RenewalStatus; + if (renewStatus === "FAILED") { + throw new AcmTerminalError(`AWS ACM renewal failed for ${certificateArn}`); + } + // ExportCertificate keeps returning the original cert until ACM has fully re-issued the + // renewed one. Serial number changing is the ground-truth signal that a new cert body + // exists; NotAfter and RenewalStatus can lag or be misleading. + const newSerial = normalizeSerial(afterRenew.Certificate?.Serial); + const renewalComplete = Boolean(newSerial && storedSerial && newSerial !== storedSerial); + if (!renewalComplete) { + throw new AcmPendingError( + `AWS ACM renewal for ${certificateArn} has not completed yet (status=${renewStatus ?? "unknown"}) — will retry` + ); + } + } + } else { + // New issuance — use the CA's configured region. + acmClient = await createAcmClient({ appConnectionId, region, appConnectionDAL, kmsService }); + + const domainName = commonName || (altNames.length > 0 ? altNames[0].value : ""); + if (!domainName) { + throw new BadRequestError({ message: "AWS ACM requires a DomainName (common name or first SAN)" }); + } + const subjectAlternativeNames = altNames.map((s) => s.value); + + const idempotencyToken = buildIdempotencyToken(certificateId); + + const requestResult = await acmClient.send( + new RequestCertificateCommand({ + DomainName: domainName, + SubjectAlternativeNames: subjectAlternativeNames.length > 0 ? subjectAlternativeNames : undefined, + KeyAlgorithm: mapCertKeyAlgorithmToAcm(keyAlgorithm), + ValidationMethod: ValidationMethod.DNS, + IdempotencyToken: idempotencyToken, + Options: { Export: CertificateExport.ENABLED } + }) + ); + + if (!requestResult.CertificateArn) { + throw new BadRequestError({ message: "AWS ACM did not return a certificate ARN" }); + } + certificateArn = requestResult.CertificateArn; + + const describe = await acmClient.send(new DescribeCertificateCommand({ CertificateArn: certificateArn })); + const detail = describe.Certificate; + if (!detail) { + throw new BadRequestError({ message: `ACM did not return details for certificate ${certificateArn}` }); + } + + if (detail.DomainValidationOptions) { + const dnsConnection = await resolveDnsAwsConnection({ + dnsAppConnectionId, + appConnectionDAL, + kmsService + }); + for (const dv of detail.DomainValidationOptions) { + if (dv.ResourceRecord?.Name && dv.ResourceRecord?.Value) { + await route53UpsertRecord(dnsConnection, hostedZoneId, { + name: dv.ResourceRecord.Name, + type: "CNAME", + value: dv.ResourceRecord.Value + }); + } + } + } + + if (detail.Status === CertificateStatus.PENDING_VALIDATION) { + throw new AcmPendingError(`AWS ACM certificate ${certificateArn} is still pending DNS validation — will retry`); + } + if ( + detail.Status === CertificateStatus.FAILED || + detail.Status === CertificateStatus.VALIDATION_TIMED_OUT || + detail.Status === CertificateStatus.REVOKED || + detail.Status === CertificateStatus.EXPIRED + ) { + throw new AcmTerminalError(`AWS ACM certificate ${certificateArn} is in terminal status: ${detail.Status}`); + } + } + + const passphrase = generateAcmPassphrase(); + let exportResult; + try { + exportResult = await acmClient.send( + new ExportCertificateCommand({ + CertificateArn: certificateArn, + Passphrase: Buffer.from(passphrase, "utf8") + }) + ); + } catch (error) { + // Right after RenewCertificate succeeds, ACM sometimes hasn't fully established the export + // relation for the renewed cert body yet and returns "must have at least one relation of type + // EXPORT". This is transient — let the queue retry loop handle it. + if (error instanceof Error && new RE2("relation of type EXPORT", "i").test(error.message)) { + throw new AcmPendingError( + `AWS ACM export not yet available for ${certificateArn} (${error.message}) — will retry` + ); + } + throw error; + } + + if (!exportResult.Certificate || !exportResult.PrivateKey) { + throw new BadRequestError({ + message: `AWS ACM ExportCertificate did not return certificate body or private key for ${certificateArn}` + }); + } + + const certificatePem = exportResult.Certificate; + const certificateChainPem = exportResult.CertificateChain || ""; + const encryptedPrivateKeyPem = exportResult.PrivateKey; + + // Decrypt AWS's encrypted private key with the ephemeral passphrase, then re-serialize as plain PKCS8. + const privateKeyObj = crypto.nativeCrypto.createPrivateKey({ + key: encryptedPrivateKeyPem, + format: "pem", + passphrase + }); + const privateKeyPem = privateKeyObj.export({ format: "pem", type: "pkcs8" }) as string; + + let certObj: x509.X509Certificate; + try { + certObj = new x509.X509Certificate(certificatePem); + } catch (error) { + throw new BadRequestError({ + message: `Failed to parse certificate from AWS ACM: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(certObj.rawData)) + }); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(privateKeyPem) + }); + + const parsedFields = extractCertificateFields(Buffer.from(certificatePem)); + + // Extract key usages and extended key usages from the certificate ACM actually issued — + // ACM applies its own policy and revises it over time, so the request is not the source of truth. + let issuedKeyUsages: CertKeyUsage[] = []; + const keyUsagesExt = certObj.getExtension(x509.KeyUsagesExtension); + if (keyUsagesExt) { + issuedKeyUsages = Object.values(CertKeyUsage).filter( + // eslint-disable-next-line no-bitwise + (usage) => (x509.KeyUsageFlags[usage] & keyUsagesExt.usages) !== 0 + ); + } + + let issuedExtendedKeyUsages: CertExtendedKeyUsage[] = []; + const extKeyUsageExt = certObj.getExtension(x509.ExtendedKeyUsageExtension); + if (extKeyUsageExt) { + issuedExtendedKeyUsages = extKeyUsageExt.usages + .map((oid) => CertExtendedKeyUsageOIDToName[oid as string]) + .filter(Boolean); + } + + // ACM picks the signature algorithm server-side — derive it from the issued cert + // so the persisted value matches what was actually signed. + const sigAlgName = certObj.signatureAlgorithm.name; + const sigHashName = (certObj.signatureAlgorithm as unknown as { hash?: { name: string } }).hash?.name; + let issuedSignatureAlgorithm: CertSignatureAlgorithm; + if (sigAlgName === "RSASSA-PKCS1-v1_5" && sigHashName === "SHA-256") { + issuedSignatureAlgorithm = CertSignatureAlgorithm.RSA_SHA256; + } else if (sigAlgName === "RSASSA-PKCS1-v1_5" && sigHashName === "SHA-384") { + issuedSignatureAlgorithm = CertSignatureAlgorithm.RSA_SHA384; + } else if (sigAlgName === "RSASSA-PKCS1-v1_5" && sigHashName === "SHA-512") { + issuedSignatureAlgorithm = CertSignatureAlgorithm.RSA_SHA512; + } else if (sigAlgName === "ECDSA" && sigHashName === "SHA-256") { + issuedSignatureAlgorithm = CertSignatureAlgorithm.ECDSA_SHA256; + } else if (sigAlgName === "ECDSA" && sigHashName === "SHA-384") { + issuedSignatureAlgorithm = CertSignatureAlgorithm.ECDSA_SHA384; + } else if (sigAlgName === "ECDSA" && sigHashName === "SHA-512") { + issuedSignatureAlgorithm = CertSignatureAlgorithm.ECDSA_SHA512; + } else { + throw new BadRequestError({ + message: `Unsupported signature algorithm from AWS ACM: ${sigAlgName} with ${sigHashName}` + }); + } + + const externalMetadata = ExternalMetadataSchema.parse({ + type: CaType.AWS_ACM_PUBLIC_CA, + arn: certificateArn, + region: issuanceRegion, + validationMethod: AwsAcmValidationMethod.DNS + }); + + let newCertId: string; + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + profileId, + status: CertStatus.ACTIVE, + friendlyName: commonName, + commonName, + altNames: altNames.map((san) => san.value).join(","), + serialNumber: certObj.serialNumber, + notBefore: certObj.notBefore, + notAfter: certObj.notAfter, + keyAlgorithm, + signatureAlgorithm: issuedSignatureAlgorithm, + keyUsages: issuedKeyUsages, + extendedKeyUsages: issuedExtendedKeyUsages, + projectId: ca.projectId, + externalMetadata, + renewedFromCertificateId: isRenewal && originalCertificateId ? originalCertificateId : null, + ...parsedFields + }, + tx + ); + + newCertId = cert.id; + + if (isRenewal && originalCertificateId) { + await certificateDAL.updateById(originalCertificateId, { renewedByCertificateId: cert.id }, tx); + } + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey + }, + tx + ); + + if (profileId && certificateProfileDAL) { + const profile = await certificateProfileDAL.findByIdWithConfigs(profileId, tx); + if (profile?.apiConfig?.autoRenew && profile.apiConfig.renewBeforeDays) { + await certificateDAL.updateById(cert.id, { renewBeforeDays: profile.apiConfig.renewBeforeDays }, tx); + } + } + }); + + return { + certificate: certificatePem, + certificateChain: certificateChainPem, + privateKey: privateKeyPem, + serialNumber: certObj.serialNumber, + certificateId: newCertId!, + ca: acmCa + }; + }; + + const revokeCertificate = async ({ + caId, + serialNumber, + reason + }: { + caId: string; + serialNumber: string; + reason: CrlReason; + }) => { + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca.externalCa || ca.externalCa.type !== CaType.AWS_ACM_PUBLIC_CA) { + throw new BadRequestError({ message: "CA is not an AWS ACM Public Certificate Authority" }); + } + + const acmCa = castDbEntryToAwsAcmPublicCaCertificateAuthority(ca); + const { appConnectionId } = acmCa.configuration; + + // ACM revokes by ARN, not serial number. Look up the ARN from the cert's externalMetadata. + const cert = await certificateDAL.findOne({ caId, serialNumber }); + if (!cert) { + throw new NotFoundError({ + message: `Certificate with serial number '${serialNumber}' not found under CA '${caId}'` + }); + } + + // If this certificate has been superseded by a renewal, the ARN now points at the renewed + // cert body in AWS — hitting AWS RevokeCertificate would revoke the actively-served cert. + // The superseded cert body is already gone from AWS, so skip the AWS call and let the caller + // mark the DB row as REVOKED on its own. + if (cert.renewedByCertificateId) { + logger.info( + `Skipping AWS ACM revoke for superseded certificate — ARN now points at renewed cert [certificateId=${cert.id}] [renewedByCertificateId=${cert.renewedByCertificateId}]` + ); + return; + } + + const parsedMetadata = ExternalMetadataSchema.safeParse(cert.externalMetadata); + if (!parsedMetadata.success || parsedMetadata.data.type !== CaType.AWS_ACM_PUBLIC_CA || !parsedMetadata.data.arn) { + throw new BadRequestError({ + message: `Certificate '${cert.id}' is missing AWS ACM metadata — cannot resolve ARN for revocation` + }); + } + + // ARNs are region-locked — use the cert's stored region, not the CA's current region. + const acmClient = await createAcmClient({ + appConnectionId, + region: parsedMetadata.data.region, + appConnectionDAL, + kmsService + }); + const revocationReason = CRL_REASON_TO_ACM_REVOCATION_REASON_MAP[reason]; + + let result; + try { + result = await acmClient.send( + new RevokeCertificateCommand({ + CertificateArn: parsedMetadata.data.arn, + RevocationReason: revocationReason + }) + ); + } catch (error) { + throw new BadRequestError({ + message: `Failed to revoke certificate via AWS Certificate Manager: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + logger.info(result, "AWS ACM RevokeCertificate result"); + }; + + return { + createCertificateAuthority, + updateCertificateAuthority, + listCertificateAuthorities, + orderCertificateFromProfile, + revokeCertificate + }; +}; + +// Re-export for existing callers (queue, v3 service, approval fns, etc.). +export { validateAcmIssuanceInputs }; diff --git a/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas.ts new file mode 100644 index 00000000000..ebbb100c58c --- /dev/null +++ b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +import { CertificateAuthorities } from "@app/lib/api-docs/constants"; +import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; + +import { CaType } from "../certificate-authority-enums"; +import { + BaseCertificateAuthoritySchema, + GenericCreateCertificateAuthorityFieldsSchema, + GenericUpdateCertificateAuthorityFieldsSchema +} from "../certificate-authority-schemas"; + +export const AwsAcmPublicCaCertificateAuthorityConfigurationSchema = z.object({ + appConnectionId: z + .string() + .uuid() + .trim() + .describe(CertificateAuthorities.CONFIGURATIONS.AWS_ACM_PUBLIC_CA.appConnectionId), + dnsAppConnectionId: z + .string() + .uuid() + .trim() + .describe(CertificateAuthorities.CONFIGURATIONS.AWS_ACM_PUBLIC_CA.dnsAppConnectionId), + hostedZoneId: z + .string() + .trim() + .min(1, "Hosted Zone ID is required") + .describe(CertificateAuthorities.CONFIGURATIONS.AWS_ACM_PUBLIC_CA.hostedZoneId), + region: z.nativeEnum(AWSRegion).describe(CertificateAuthorities.CONFIGURATIONS.AWS_ACM_PUBLIC_CA.region) +}); + +export const AwsAcmPublicCaCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({ + type: z.literal(CaType.AWS_ACM_PUBLIC_CA), + configuration: AwsAcmPublicCaCertificateAuthorityConfigurationSchema +}); + +export const CreateAwsAcmPublicCaCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema( + CaType.AWS_ACM_PUBLIC_CA +).extend({ + configuration: AwsAcmPublicCaCertificateAuthorityConfigurationSchema +}); + +export const UpdateAwsAcmPublicCaCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema( + CaType.AWS_ACM_PUBLIC_CA +).extend({ + configuration: AwsAcmPublicCaCertificateAuthorityConfigurationSchema.optional() +}); diff --git a/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-types.ts b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-types.ts new file mode 100644 index 00000000000..680d46e4b8a --- /dev/null +++ b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-types.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +import { + AwsAcmPublicCaCertificateAuthoritySchema, + CreateAwsAcmPublicCaCertificateAuthoritySchema, + UpdateAwsAcmPublicCaCertificateAuthoritySchema +} from "./aws-acm-public-ca-certificate-authority-schemas"; + +export type TAwsAcmPublicCaCertificateAuthority = z.infer; + +export type TCreateAwsAcmPublicCaCertificateAuthorityDTO = z.infer< + typeof CreateAwsAcmPublicCaCertificateAuthoritySchema +>; + +export type TUpdateAwsAcmPublicCaCertificateAuthorityDTO = z.infer< + typeof UpdateAwsAcmPublicCaCertificateAuthoritySchema +>; diff --git a/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-validators.ts b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-validators.ts new file mode 100644 index 00000000000..778202026e4 --- /dev/null +++ b/backend/src/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-validators.ts @@ -0,0 +1,128 @@ +import { customAlphabet } from "nanoid"; + +import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { CertKeyAlgorithm, CertSubjectAlternativeNameType } from "@app/services/certificate/certificate-types"; + +import { AWS_ACM_CERTIFICATE_VALIDITY_DAYS } from "./aws-acm-public-ca-certificate-authority-enums"; + +export const ACM_ALLOWED_KEY_ALGORITHMS = new Set([ + CertKeyAlgorithm.RSA_2048, + CertKeyAlgorithm.ECDSA_P256, + CertKeyAlgorithm.ECDSA_P384 +]); + +export const ACM_FIXED_VALIDITY_MS = AWS_ACM_CERTIFICATE_VALIDITY_DAYS * 24 * 60 * 60 * 1000; + +/** + * Pre-flight validator for ACM issuance inputs. Called both by the async fns + * (defense in depth) and synchronously by the certificate order API before + * enqueuing, so the user gets a 400 on submit rather than a FAILED request + * row a moment later. + */ +export const validateAcmIssuanceInputs = ({ + csr, + keyAlgorithm, + altNames, + ttl, + notBefore, + notAfter, + organization, + organizationalUnit, + country, + state, + locality, + isRenewal +}: { + csr?: string; + keyAlgorithm?: string; + altNames?: Array<{ type: CertSubjectAlternativeNameType; value: string }>; + ttl?: string; + notBefore?: Date | string; + notAfter?: Date | string; + organization?: string; + organizationalUnit?: string; + country?: string; + state?: string; + locality?: string; + isRenewal?: boolean; +}) => { + if (csr) { + throw new BadRequestError({ + message: "AWS Certificate Manager does not support CSR-based issuance" + }); + } + if (keyAlgorithm && !ACM_ALLOWED_KEY_ALGORITHMS.has(keyAlgorithm)) { + throw new BadRequestError({ + message: `AWS ACM only supports RSA_2048, EC_prime256v1, and EC_secp384r1 key algorithms. Received: ${keyAlgorithm}` + }); + } + if (organization || organizationalUnit || country || state || locality) { + throw new BadRequestError({ + message: "AWS Certificate Manager does not support subject fields (O, OU, C, ST, L)" + }); + } + if (altNames) { + for (const san of altNames) { + if (san.type !== CertSubjectAlternativeNameType.DNS_NAME) { + throw new BadRequestError({ + message: `AWS Certificate Manager only supports DNS SANs. Unsupported SAN type: ${san.type}` + }); + } + } + } + // On renewal, ACM handles validity itself — we don't pass a TTL to AWS, and the + // TTL derived from the original cert may round down (e.g., 197.999d → "197d"), + // so skip the exact-match check. + if (!isRenewal) { + if (!ttl) { + throw new BadRequestError({ + message: `AWS Certificate Manager issues certificates with a fixed validity of ${AWS_ACM_CERTIFICATE_VALIDITY_DAYS} days.` + }); + } + let ttlMs: number; + try { + ttlMs = ms(ttl); + } catch { + throw new BadRequestError({ + message: `Invalid TTL format: ${ttl}` + }); + } + if (ttlMs !== ACM_FIXED_VALIDITY_MS) { + throw new BadRequestError({ + message: `AWS Certificate Manager issues certificates with a fixed validity of ${AWS_ACM_CERTIFICATE_VALIDITY_DAYS} days.` + }); + } + if (notBefore || notAfter) { + throw new BadRequestError({ + message: `AWS Certificate Manager does not support notBefore or notAfter — validity is fixed at ${AWS_ACM_CERTIFICATE_VALIDITY_DAYS} days from issuance.` + }); + } + } +}; + +export const mapCertKeyAlgorithmToAcm = (keyAlgorithm: CertKeyAlgorithm) => { + switch (keyAlgorithm) { + case CertKeyAlgorithm.RSA_2048: + return "RSA_2048"; + case CertKeyAlgorithm.ECDSA_P256: + return "EC_prime256v1"; + case CertKeyAlgorithm.ECDSA_P384: + return "EC_secp384r1"; + default: + throw new BadRequestError({ + message: `AWS ACM only supports RSA_2048, EC_prime256v1, and EC_secp384r1 key algorithms. Received: ${keyAlgorithm as string}` + }); + } +}; + +// ACM's ExportCertificate passphrase must be 4-128 chars and cannot contain #, $, or %. +const generateAcmPassphraseInternal = customAlphabet( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + 32 +); +export const generateAcmPassphrase = (): string => generateAcmPassphraseInternal(); + +// Strip hyphens from the certificate UUID to produce a 32-char token that +// satisfies AWS's IdempotencyToken constraints (max 32 chars, alphanumeric). +export const buildIdempotencyToken = (certificateId: string) => certificateId.split("-").join("").slice(0, 32); diff --git a/backend/src/services/certificate-authority/certificate-authority-enums.ts b/backend/src/services/certificate-authority/certificate-authority-enums.ts index 7b56dc1bb0e..8919cc53d05 100644 --- a/backend/src/services/certificate-authority/certificate-authority-enums.ts +++ b/backend/src/services/certificate-authority/certificate-authority-enums.ts @@ -2,7 +2,8 @@ export enum CaType { INTERNAL = "internal", ACME = "acme", AZURE_AD_CS = "azure-ad-cs", - AWS_PCA = "aws-pca" + AWS_PCA = "aws-pca", + AWS_ACM_PUBLIC_CA = "aws-acm-public-ca" } export enum InternalCaType { diff --git a/backend/src/services/certificate-authority/certificate-authority-maps.ts b/backend/src/services/certificate-authority/certificate-authority-maps.ts index 0fc093a1a3f..77916adbe32 100644 --- a/backend/src/services/certificate-authority/certificate-authority-maps.ts +++ b/backend/src/services/certificate-authority/certificate-authority-maps.ts @@ -4,7 +4,8 @@ export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record = { [CaType.INTERNAL]: "Internal", [CaType.ACME]: "ACME-compatible CA", [CaType.AZURE_AD_CS]: "Active Directory Certificate Service", - [CaType.AWS_PCA]: "AWS Private Certificate Authority" + [CaType.AWS_PCA]: "AWS Private Certificate Authority", + [CaType.AWS_ACM_PUBLIC_CA]: "AWS ACM Public CA" }; export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record = { @@ -19,7 +20,16 @@ export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record; permissionService: Pick; - certificateDAL: Pick; + certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; kmsService: Pick< @@ -87,7 +95,7 @@ type TCertificateAuthorityServiceFactoryDep = { pkiSubscriberDAL: Pick; pkiSyncDAL: Pick; pkiSyncQueue: Pick; - certificateProfileDAL?: Pick; + certificateProfileDAL?: Pick; }; export type TCertificateAuthorityServiceFactory = ReturnType; @@ -154,6 +162,19 @@ export const certificateAuthorityServiceFactory = ({ certificateProfileDAL }); + const awsAcmPublicCaFns = AwsAcmPublicCaCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + certificateProfileDAL + }); + const createCertificateAuthority = async ( { type, projectId, name, configuration, status }: TCreateCertificateAuthorityDTO, actor: OrgServiceActor @@ -227,6 +248,16 @@ export const certificateAuthorityServiceFactory = ({ }); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return awsAcmPublicCaFns.createCertificateAuthority({ + name, + projectId, + configuration: configuration as TCreateAwsAcmPublicCaCertificateAuthorityDTO["configuration"], + status, + actor + }); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -289,6 +320,10 @@ export const certificateAuthorityServiceFactory = ({ return castDbEntryToAwsPcaCertificateAuthority(certificateAuthority); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return castDbEntryToAwsAcmPublicCaCertificateAuthority(certificateAuthority); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -356,6 +391,10 @@ export const certificateAuthorityServiceFactory = ({ return castDbEntryToAwsPcaCertificateAuthority(certificateAuthority); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return castDbEntryToAwsAcmPublicCaCertificateAuthority(certificateAuthority); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -418,6 +457,10 @@ export const certificateAuthorityServiceFactory = ({ return awsPcaFns.listCertificateAuthorities({ projectId, permissionFilters }); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return awsAcmPublicCaFns.listCertificateAuthorities({ projectId, permissionFilters }); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -507,6 +550,16 @@ export const certificateAuthorityServiceFactory = ({ }); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return awsAcmPublicCaFns.updateCertificateAuthority({ + id: certificateAuthority.id, + configuration: configuration as TUpdateAwsAcmPublicCaCertificateAuthorityDTO["configuration"], + actor, + status, + name + }); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -570,6 +623,10 @@ export const certificateAuthorityServiceFactory = ({ return castDbEntryToAwsPcaCertificateAuthority(certificateAuthority); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return castDbEntryToAwsAcmPublicCaCertificateAuthority(certificateAuthority); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -662,6 +719,16 @@ export const certificateAuthorityServiceFactory = ({ }); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return awsAcmPublicCaFns.updateCertificateAuthority({ + id: certificateAuthority.id, + configuration: configuration as TUpdateAwsAcmPublicCaCertificateAuthorityDTO["configuration"], + actor, + status, + name + }); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -731,6 +798,10 @@ export const certificateAuthorityServiceFactory = ({ return castDbEntryToAwsPcaCertificateAuthority(certificateAuthority); } + if (type === CaType.AWS_ACM_PUBLIC_CA) { + return castDbEntryToAwsAcmPublicCaCertificateAuthority(certificateAuthority); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -840,6 +911,11 @@ export const certificateAuthorityServiceFactory = ({ return; } + if (caType === CaType.AWS_ACM_PUBLIC_CA) { + await awsAcmPublicCaFns.revokeCertificate({ caId, serialNumber, reason }); + return; + } + throw new BadRequestError({ message: `Certificate revocation via CA service is not supported for CA type "${caType}"` }); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index d3eb1257a97..fa570af41f3 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -1,4 +1,8 @@ import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types"; +import { + TAwsAcmPublicCaCertificateAuthority, + TCreateAwsAcmPublicCaCertificateAuthorityDTO +} from "./aws-acm-public-ca/aws-acm-public-ca-certificate-authority-types"; import { TAwsPcaCertificateAuthority, TCreateAwsPcaCertificateAuthorityDTO @@ -17,13 +21,15 @@ export type TCertificateAuthority = | TInternalCertificateAuthority | TAcmeCertificateAuthority | TAzureAdCsCertificateAuthority - | TAwsPcaCertificateAuthority; + | TAwsPcaCertificateAuthority + | TAwsAcmPublicCaCertificateAuthority; export type TCertificateAuthorityInput = | TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput | TCreateAzureAdCsCertificateAuthorityDTO - | TCreateAwsPcaCertificateAuthorityDTO; + | TCreateAwsPcaCertificateAuthorityDTO + | TCreateAwsAcmPublicCaCertificateAuthorityDTO; export type TCreateCertificateAuthorityDTO = Omit; diff --git a/backend/src/services/certificate-authority/certificate-issuance-queue.ts b/backend/src/services/certificate-authority/certificate-issuance-queue.ts index aae303ca7ce..052d8d06708 100644 --- a/backend/src/services/certificate-authority/certificate-issuance-queue.ts +++ b/backend/src/services/certificate-authority/certificate-issuance-queue.ts @@ -1,4 +1,5 @@ import acme from "acme-client"; +import { UnrecoverableError } from "bullmq"; import { crypto } from "@app/lib/crypto/cryptography"; import { NotFoundError } from "@app/lib/errors"; @@ -29,6 +30,8 @@ import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { copyMetadataFromRequestToCertificate } from "../resource-metadata/resource-metadata-fns"; import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; +import { AcmPendingError } from "./aws-acm-public-ca/aws-acm-public-ca-certificate-authority-errors"; +import { AwsAcmPublicCaCertificateAuthorityFns } from "./aws-acm-public-ca/aws-acm-public-ca-certificate-authority-fns"; import { AwsPcaCertificateAuthorityFns } from "./aws-pca/aws-pca-certificate-authority-fns"; import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; @@ -69,6 +72,7 @@ export type TIssueCertificateFromProfileJobData = { certificateId: string; profileId: string; caId: string; + caType?: CaType; commonName?: string; altNames?: Array<{ type: string; value: string }>; ttl: string; @@ -104,7 +108,7 @@ type TCertificateIssuanceQueueFactoryDep = { pkiSubscriberDAL: Pick; pkiSyncDAL: Pick; pkiSyncQueue: Pick; - certificateProfileDAL?: Pick; + certificateProfileDAL?: Pick; certificateRequestService?: Pick< TCertificateRequestServiceFactory, "attachCertificateToRequest" | "updateCertificateRequestStatus" @@ -179,6 +183,19 @@ export const certificateIssuanceQueueFactory = ({ certificateProfileDAL }); + const awsAcmPublicCaFns = AwsAcmPublicCaCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + certificateProfileDAL + }); + /** * Queue a certificate issuance job. */ @@ -186,6 +203,7 @@ export const certificateIssuanceQueueFactory = ({ certificateId, profileId, caId, + caType, commonName, altNames, ttl, @@ -207,6 +225,7 @@ export const certificateIssuanceQueueFactory = ({ certificateId, profileId, caId, + caType, commonName, altNames, ttl, @@ -225,13 +244,16 @@ export const certificateIssuanceQueueFactory = ({ locality }; + // ACM DNS validation can take 5–30 minutes; the function is fully idempotent via + // IdempotencyToken, so we poll longer with a fixed backoff instead of exponential. + const queueOpts = + caType === CaType.AWS_ACM_PUBLIC_CA + ? { attempts: 30, backoff: { type: "fixed" as const, delay: 60000 } } + : { attempts: 3, backoff: { type: "exponential" as const, delay: 5000 } }; + await queueService.queue(QueueName.CertificateIssuance, QueueJobs.CaIssueCertificateFromProfile, jobData, { jobId: `certificate-issuance-${certificateId}`, - attempts: 3, - backoff: { - type: "exponential", - delay: 5000 - } + ...queueOpts }); }; @@ -396,6 +418,62 @@ export const certificateIssuanceQueueFactory = ({ certificateId: azureResult.certificateId }); + logger.info(`Certificate attached to request [certificateRequestId=${certificateRequestId}]`); + } catch (attachError) { + logger.error( + attachError, + `Failed to attach certificate to request [certificateRequestId=${certificateRequestId}]` + ); + try { + await certificateRequestService.updateCertificateRequestStatus({ + certificateRequestId, + status: CertificateRequestStatus.FAILED, + errorMessage: `Failed to attach certificate: ${attachError instanceof Error ? attachError.message : String(attachError)}` + }); + } catch (statusUpdateError) { + logger.error( + statusUpdateError, + `Failed to update certificate request status [certificateRequestId=${certificateRequestId}]` + ); + } + } + } + } else if (ca.externalCa?.type === CaType.AWS_ACM_PUBLIC_CA) { + const acmParams = { + caId, + profileId, + certificateId, + commonName: commonName || "", + altNames: (altNames || []) as Array<{ type: CertSubjectAlternativeNameType; value: string }>, + keyUsages, + extendedKeyUsages, + validity: { ttl }, + signatureAlgorithm, + keyAlgorithm: keyAlgorithm as CertKeyAlgorithm, + isRenewal, + originalCertificateId, + ...(csr && { csr }), + organization, + organizationalUnit, + country, + state, + locality + }; + + const acmResult = await awsAcmPublicCaFns.orderCertificateFromProfile(acmParams); + + if (certificateRequestId && certificateRequestService && acmResult?.certificateId) { + try { + await certificateRequestService.attachCertificateToRequest({ + certificateRequestId, + certificateId: acmResult.certificateId + }); + + await copyMetadataFromRequestToCertificate(resourceMetadataDAL, { + certificateRequestId, + certificateId: acmResult.certificateId + }); + logger.info(`Certificate attached to request [certificateRequestId=${certificateRequestId}]`); } catch (attachError) { logger.error( @@ -487,6 +565,16 @@ export const certificateIssuanceQueueFactory = ({ logger.debug("Failed to queue PKI alert event for async certificate issuance"); } } catch (error: unknown) { + // AcmPendingError signals that an ACM operation (DNS validation, renewal, export) is still + // in flight. Don't mark the request as FAILED on every poll — only after the queue exhausts attempts. + const isRetryable = error instanceof AcmPendingError; + if (isRetryable) { + logger.info( + `Certificate issuance pending ACM operation — will retry [certificateId=${certificateId}] [caId=${caId}]` + ); + throw error; + } + logger.error(error, `Certificate issuance job failed for [certificateId=${certificateId}] [caId=${caId}]`); if (certificateRequestId && certificateRequestService) { @@ -505,12 +593,52 @@ export const certificateIssuanceQueueFactory = ({ } } + // For ACM's 30-attempt queue, wrap non-retryable errors so BullMQ stops retrying immediately. + // Other CAs keep default retry behavior (3 attempts is short enough that running through them is fine). + if (data.caType === CaType.AWS_ACM_PUBLIC_CA) { + const message = error instanceof Error ? error.message : String(error); + const wrapped = new UnrecoverableError(message); + (wrapped as Error).cause = error; + throw wrapped; + } + throw error; } }; queueService.start(QueueName.CertificateIssuance, async (job) => { - await processCertificateIssuanceJobs(job.data); + try { + await processCertificateIssuanceJobs(job.data); + } catch (error) { + // AcmPendingError is rethrown on every retry so BullMQ keeps polling; the in-handler + // FAILED-update branch never runs for it. On the final attempt we still need to flip the request + // row to FAILED ourselves — BullMQ will move the job to the failed state but has no hook to + // update our DB, and no queue-level "failed" listener is wired for CertificateIssuance. + if (error instanceof AcmPendingError) { + const attemptsMade = job.attemptsMade ?? 0; + const maxAttempts = job.opts?.attempts ?? 1; + const isFinalAttempt = attemptsMade + 1 >= maxAttempts; + const { certificateRequestId, certificateId, caId } = job.data; + if (isFinalAttempt && certificateRequestId && certificateRequestService) { + try { + await certificateRequestService.updateCertificateRequestStatus({ + certificateRequestId, + status: CertificateRequestStatus.FAILED, + errorMessage: `AWS ACM DNS validation did not complete after ${maxAttempts} attempts: ${error.message}` + }); + logger.info( + `Marked certificate request FAILED after exhausted ACM validation retries [certificateRequestId=${certificateRequestId}] [certificateId=${certificateId}] [caId=${caId}]` + ); + } catch (updateError) { + logger.error( + updateError, + `Failed to mark certificate request FAILED after exhausted ACM retries [certificateRequestId=${certificateRequestId}]` + ); + } + } + } + throw error; + } }); return { diff --git a/backend/src/services/certificate-authority/dns-providers/route53.ts b/backend/src/services/certificate-authority/dns-providers/route53.ts new file mode 100644 index 00000000000..cb14c242dce --- /dev/null +++ b/backend/src/services/certificate-authority/dns-providers/route53.ts @@ -0,0 +1,66 @@ +import { ChangeResourceRecordSetsCommand, GetHostedZoneCommand, Route53Client } from "@aws-sdk/client-route-53"; + +import { CustomAWSHasher } from "@app/lib/aws/hashing"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types"; + +export type TRoute53Record = { + name: string; + type: "CNAME" | "TXT" | "A" | "AAAA"; + value: string; + ttl?: number; + comment?: string; +}; + +const buildClient = async (connection: TAwsConnectionConfig) => { + // Route 53 is a global service — the region passed here only affects the signer, not the data plane. + // us-east-1 is AWS's canonical region for global services. + const config = await getAwsConnectionConfig(connection, AWSRegion.US_EAST_1); + return new Route53Client({ + sha256: CustomAWSHasher, + useFipsEndpoint: crypto.isFipsModeEnabled(), + credentials: config.credentials, + region: config.region + }); +}; + +const changeRecord = async ( + connection: TAwsConnectionConfig, + hostedZoneId: string, + action: "UPSERT" | "DELETE", + record: TRoute53Record +) => { + const route53Client = await buildClient(connection); + const defaultComment = `${action === "UPSERT" ? "Upsert" : "Delete"} ${record.type} record for ${record.name}`; + const command = new ChangeResourceRecordSetsCommand({ + HostedZoneId: hostedZoneId, + ChangeBatch: { + Comment: record.comment ?? defaultComment, + Changes: [ + { + Action: action, + ResourceRecordSet: { + Name: record.name, + Type: record.type, + TTL: record.ttl ?? 300, + ResourceRecords: [{ Value: record.value }] + } + } + ] + } + }); + await route53Client.send(command); +}; + +export const route53UpsertRecord = (connection: TAwsConnectionConfig, hostedZoneId: string, record: TRoute53Record) => + changeRecord(connection, hostedZoneId, "UPSERT", record); + +export const route53DeleteRecord = (connection: TAwsConnectionConfig, hostedZoneId: string, record: TRoute53Record) => + changeRecord(connection, hostedZoneId, "DELETE", record); + +export const route53GetHostedZone = async (connection: TAwsConnectionConfig, hostedZoneId: string) => { + const route53Client = await buildClient(connection); + await route53Client.send(new GetHostedZoneCommand({ Id: hostedZoneId })); +}; diff --git a/backend/src/services/certificate-common/external-metadata-schemas.ts b/backend/src/services/certificate-common/external-metadata-schemas.ts new file mode 100644 index 00000000000..46e6d6e2d47 --- /dev/null +++ b/backend/src/services/certificate-common/external-metadata-schemas.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { AwsAcmValidationMethod } from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-enums"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +export const AwsAcmPublicCaExternalMetadataSchema = z.object({ + type: z.literal(CaType.AWS_ACM_PUBLIC_CA), + arn: z.string(), + region: z.nativeEnum(AWSRegion), + validationMethod: z.nativeEnum(AwsAcmValidationMethod) +}); + +export type TAwsAcmPublicCaExternalMetadata = z.infer; + +export const ExternalMetadataSchema = z.discriminatedUnion("type", [AwsAcmPublicCaExternalMetadataSchema]); + +export type TExternalMetadata = z.infer; diff --git a/backend/src/services/certificate-profile/certificate-profile-external-config-schemas.ts b/backend/src/services/certificate-profile/certificate-profile-external-config-schemas.ts index 7ffdc896494..e4ef6620f26 100644 --- a/backend/src/services/certificate-profile/certificate-profile-external-config-schemas.ts +++ b/backend/src/services/certificate-profile/certificate-profile-external-config-schemas.ts @@ -22,6 +22,11 @@ export const AcmeExternalConfigSchema = z.object({}); */ export const AwsPcaExternalConfigSchema = z.object({}); +/** + * External configuration schema for AWS ACM Public Certificate Authority + */ +export const AwsAcmPublicCaExternalConfigSchema = z.object({}); + /** * Map of CA types to their corresponding external configuration schemas */ @@ -29,6 +34,7 @@ export const ExternalConfigSchemaMap = { [CaType.AZURE_AD_CS]: AzureAdCsExternalConfigSchema, [CaType.ACME]: AcmeExternalConfigSchema, [CaType.AWS_PCA]: AwsPcaExternalConfigSchema, + [CaType.AWS_ACM_PUBLIC_CA]: AwsAcmPublicCaExternalConfigSchema, [CaType.INTERNAL]: z.object({}).optional() // Internal CAs don't use external configs } as const; @@ -49,7 +55,13 @@ export const createExternalConfigSchema = (caType?: CaType | null) => { * Union type of all possible external configuration schemas */ export const ExternalConfigUnionSchema = z - .union([AzureAdCsExternalConfigSchema, AcmeExternalConfigSchema, AwsPcaExternalConfigSchema, z.object({})]) + .union([ + AzureAdCsExternalConfigSchema, + AcmeExternalConfigSchema, + AwsPcaExternalConfigSchema, + AwsAcmPublicCaExternalConfigSchema, + z.object({}) + ]) .nullable() .optional(); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 784d1f2f44c..8d3cf5ab7b5 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -101,6 +101,20 @@ const validateTemplateByExternalCaType = ( } }; +const validateAcmEnrollmentType = async ( + caId: string | null | undefined, + enrollmentType: EnrollmentType, + externalCertificateAuthorityDAL: Pick +) => { + if (!caId) return; + const externalCa = await externalCertificateAuthorityDAL.findOne({ caId }); + if (externalCa?.type === CaType.AWS_ACM_PUBLIC_CA && enrollmentType !== EnrollmentType.API) { + throw new ForbiddenRequestError({ + message: "AWS Certificate Manager only supports API enrollment" + }); + } +}; + const validateExternalConfigs = async ( externalConfigs: Record | null | undefined, caId: string | null, @@ -355,6 +369,8 @@ export const certificateProfileServiceFactory = ({ validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null); + await validateAcmEnrollmentType(data.caId, data.enrollmentType, externalCertificateAuthorityDAL); + // Validate defaults against policy constraints if (data.defaults && data.certificatePolicyId) { const policy = await certificatePolicyDAL.findById(data.certificatePolicyId); @@ -621,6 +637,8 @@ export const certificateProfileServiceFactory = ({ validateIssuerTypeConstraints(finalIssuerType, finalEnrollmentType, finalCaId ?? null, existingProfile.caId); + await validateAcmEnrollmentType(finalCaId, finalEnrollmentType, externalCertificateAuthorityDAL); + // Validate external configs only if they are provided in the update if (data.externalConfigs !== undefined) { await validateExternalConfigs( diff --git a/backend/src/services/certificate-v3/certificate-approval-fns.ts b/backend/src/services/certificate-v3/certificate-approval-fns.ts index ef1139dc526..8dc5e6ddb05 100644 --- a/backend/src/services/certificate-v3/certificate-approval-fns.ts +++ b/backend/src/services/certificate-v3/certificate-approval-fns.ts @@ -14,6 +14,7 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { CertKeyAlgorithm, CertSignatureAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; +import { validateAcmIssuanceInputs } from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-fns"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { TCertificateIssuanceQueueFactory } from "@app/services/certificate-authority/certificate-issuance-queue"; @@ -438,16 +439,38 @@ export const certificateApprovalServiceFactory = ( const caType = (targetCa.externalCa?.type as CaType) ?? CaType.INTERNAL; - if (caType !== CaType.ACME && caType !== CaType.AZURE_AD_CS && caType !== CaType.AWS_PCA) { + if ( + caType !== CaType.ACME && + caType !== CaType.AZURE_AD_CS && + caType !== CaType.AWS_PCA && + caType !== CaType.AWS_ACM_PUBLIC_CA + ) { return null; } + // Pre-flight validation for ACM — fail the approval synchronously rather than + // letting the job produce a FAILED request row after the approver already accepted. + if (caType === CaType.AWS_ACM_PUBLIC_CA) { + validateAcmIssuanceInputs({ + csr: certRequest.csr || undefined, + keyAlgorithm: certRequest.keyAlgorithm || undefined, + altNames: altNames ?? undefined, + ttl, + organization: certRequest.organization || undefined, + organizationalUnit: certRequest.organizationalUnit || undefined, + country: certRequest.country || undefined, + state: certRequest.state || undefined, + locality: certRequest.locality || undefined + }); + } + const orderId = randomUUID(); await certificateIssuanceQueue.queueCertificateIssuance({ certificateId: orderId, profileId: profile.id, caId: profile.caId || "", + caType, ttl: ttl || "1y", signatureAlgorithm: certRequest.signatureAlgorithm || "", keyAlgorithm: certRequest.keyAlgorithm || "", diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 81e2731891c..3898602fdcd 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -34,6 +34,7 @@ import { CertSignatureAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; +import { validateAcmIssuanceInputs } from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-fns"; import { TCertificateAuthorityDALFactory, TCertificateAuthorityWithAssociatedCa @@ -276,7 +277,11 @@ const validateRenewalEligibility = ( const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; const isInternalCa = caType === CaType.INTERNAL; - const isConnectedExternalCa = caType === CaType.ACME || caType === CaType.AZURE_AD_CS || caType === CaType.AWS_PCA; + const isConnectedExternalCa = + caType === CaType.ACME || + caType === CaType.AZURE_AD_CS || + caType === CaType.AWS_PCA || + caType === CaType.AWS_ACM_PUBLIC_CA; const isImportedCertificate = certificate.pkiSubscriberId != null && !certificate.profileId; if (!isInternalCa && !isConnectedExternalCa) { @@ -1709,6 +1714,28 @@ export const certificateV3ServiceFactory = ({ }); } + // ACM pre-flight validation runs before the approval branch so bad inputs (e.g., a TTL that + // isn't ACM's fixed 198 days) are rejected at submit time rather than after the approver has + // already approved a request that's guaranteed to fail downstream. + if (profile.caId) { + const preflightCa = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); + if (preflightCa?.externalCa?.type === CaType.AWS_ACM_PUBLIC_CA) { + validateAcmIssuanceInputs({ + csr: certificateOrder.csr, + keyAlgorithm: certificateOrder.keyAlgorithm, + altNames: certificateOrder.altNames, + ttl: certificateOrder.validity?.ttl, + notBefore: certificateOrder.notBefore, + notAfter: certificateOrder.notAfter, + organization: certificateRequest.organization, + organizationalUnit: certificateRequest.organizationalUnit, + country: certificateRequest.country, + state: certificateRequest.state, + locality: certificateRequest.locality + }); + } + } + const orderApprovalFactory = APPROVAL_POLICY_FACTORY_MAP[ApprovalPolicyType.CertRequest]( ApprovalPolicyType.CertRequest ); @@ -1853,7 +1880,30 @@ export const certificateV3ServiceFactory = ({ }); } - if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS || caType === CaType.AWS_PCA) { + if ( + caType === CaType.ACME || + caType === CaType.AZURE_AD_CS || + caType === CaType.AWS_PCA || + caType === CaType.AWS_ACM_PUBLIC_CA + ) { + // Pre-flight validation for ACM — reject bad inputs synchronously so the user + // gets a 400 on submit rather than a FAILED request row after the job runs. + if (caType === CaType.AWS_ACM_PUBLIC_CA) { + validateAcmIssuanceInputs({ + csr: certificateOrder.csr, + keyAlgorithm: certificateOrder.keyAlgorithm, + altNames: certificateOrder.altNames, + ttl: certificateOrder.validity?.ttl, + notBefore: certificateOrder.notBefore, + notAfter: certificateOrder.notAfter, + organization: certificateRequest.organization, + organizationalUnit: certificateRequest.organizationalUnit, + country: certificateRequest.country, + state: certificateRequest.state, + locality: certificateRequest.locality + }); + } + const orderId = randomUUID(); const certRequest = await certificateRequestService.createCertificateRequest({ @@ -1895,6 +1945,7 @@ export const certificateV3ServiceFactory = ({ certificateId: orderId, profileId: profile.id, caId: profile.caId || "", + caType, ttl: certificateOrder.validity?.ttl || "1y", signatureAlgorithm: certificateOrder.signatureAlgorithm || "", keyAlgorithm: certificateRequest.keyAlgorithm || "", @@ -2181,7 +2232,12 @@ export const certificateV3ServiceFactory = ({ throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); } newCert = foundCert; - } else if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS || caType === CaType.AWS_PCA) { + } else if ( + caType === CaType.ACME || + caType === CaType.AZURE_AD_CS || + caType === CaType.AWS_PCA || + caType === CaType.AWS_ACM_PUBLIC_CA + ) { // External CA renewal - mark for async processing outside transaction return { isExternalCA: true, @@ -2361,6 +2417,7 @@ export const certificateV3ServiceFactory = ({ certificateId: renewalOrderId, profileId: profile?.id || "", caId: ca.id, + caType: (ca.externalCa?.type as CaType) ?? CaType.INTERNAL, commonName: originalCert.commonName || "", altNames: structuredAltNames, ttl, diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 0c50497be8a..d6de2839ab1 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -388,6 +388,16 @@ export const certificateServiceFactory = ({ if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked"); + // Call the upstream CA first so we don't end up with a cert that's revoked locally but still + // active at the issuer (e.g., when the upstream rejects the chosen revocation reason). + if (ca.externalCa?.type === CaType.AWS_PCA || ca.externalCa?.type === CaType.AWS_ACM_PUBLIC_CA) { + await certificateAuthorityService.revokeCertificate({ + caId: ca.id, + serialNumber: cert.serialNumber, + reason: revocationReason + }); + } + const revokedAt = new Date(); await certificateDAL.update( { @@ -407,17 +417,6 @@ export const certificateServiceFactory = ({ pkiSyncQueue }); - // Note: External CA revocation handling would go here for supported CA types - // Currently, only internal CAs, ACME CAs and AWS PCA (external CA) support revocation - - if (ca.externalCa?.type === CaType.AWS_PCA) { - await certificateAuthorityService.revokeCertificate({ - caId: ca.id, - serialNumber: cert.serialNumber, - reason: revocationReason - }); - } - // rebuild CRL (TODO: move to interval-based cron job) // Only rebuild CRL for internal CAs - external CAs manage their own CRLs if (!ca.externalCa?.id) { diff --git a/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/create.mdx b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/create.mdx new file mode 100644 index 00000000000..fcdc9d688eb --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/cert-manager/ca/aws-acm-public-ca" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/delete.mdx b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/delete.mdx new file mode 100644 index 00000000000..20ac30310d9 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/cert-manager/ca/aws-acm-public-ca/{id}" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/list.mdx b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/list.mdx new file mode 100644 index 00000000000..296343acf23 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/cert-manager/ca/aws-acm-public-ca" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/read.mdx b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/read.mdx new file mode 100644 index 00000000000..1080ee62ede --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/read.mdx @@ -0,0 +1,4 @@ +--- +title: "Read" +openapi: "GET /api/v1/cert-manager/ca/aws-acm-public-ca/{id}" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/update.mdx b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/update.mdx new file mode 100644 index 00000000000..ac77c3d006d --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/aws-acm-public-ca/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/cert-manager/ca/aws-acm-public-ca/{id}" +--- diff --git a/docs/docs.json b/docs/docs.json index f00db502ad8..a35fb61b064 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -833,6 +833,7 @@ "documentation/platform/pki/ca/sectigo", "documentation/platform/pki/ca/azure-adcs", "documentation/platform/pki/ca/aws-pca", + "documentation/platform/pki/ca/aws-acm-public-ca", "documentation/platform/pki/ca/venafi" ] } @@ -2930,6 +2931,16 @@ "api-reference/endpoints/certificate-authorities/aws-pca/delete" ] }, + { + "group": "AWS ACM Public CA", + "pages": [ + "api-reference/endpoints/certificate-authorities/aws-acm-public-ca/list", + "api-reference/endpoints/certificate-authorities/aws-acm-public-ca/create", + "api-reference/endpoints/certificate-authorities/aws-acm-public-ca/read", + "api-reference/endpoints/certificate-authorities/aws-acm-public-ca/update", + "api-reference/endpoints/certificate-authorities/aws-acm-public-ca/delete" + ] + }, { "group": "Internal", "pages": [ diff --git a/docs/documentation/platform/pki/ca/aws-acm-public-ca.mdx b/docs/documentation/platform/pki/ca/aws-acm-public-ca.mdx new file mode 100644 index 00000000000..d9f2ae91608 --- /dev/null +++ b/docs/documentation/platform/pki/ca/aws-acm-public-ca.mdx @@ -0,0 +1,174 @@ +--- +title: "AWS ACM Public CA" +description: "Issue and manage publicly-trusted certificates using AWS Certificate Manager (ACM) with Infisical." +--- + +## Overview + +Infisical integrates with AWS Certificate Manager (ACM) to issue **public certificates** signed by [Amazon Trust Services](https://www.amazontrust.com/repository/). These certificates are trusted by all major browsers and operating systems out of the box, so they can be used on the public internet without users having to install anything. + +Common use cases include securing public-facing websites and APIs, terminating TLS on internet-facing load balancers, and issuing certificates for SaaS applications exposed to external users. + +Each certificate has a fixed 198-day validity and is generated and stored by AWS. Infisical orchestrates the full lifecycle on top: domain validation via Route 53, saving the certificate and private key into Infisical, scheduled auto-renewal, and revocation. + + + Domain validation is performed exclusively through **Amazon Route 53**. Other DNS providers are not supported for this CA type. + + +## Prerequisites + +- Two [AWS App Connections](/integrations/app-connections/aws): one for ACM, one for Route 53. They can be the same connection if it has permissions for both services. +- A Route 53 **public** hosted zone for the domains you will issue certificates for. + +### IAM Permissions + +**ACM connection** — needs the following on certificates in your account: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "acm:RequestCertificate", + "acm:DescribeCertificate", + "acm:ExportCertificate", + "acm:RenewCertificate", + "acm:RevokeCertificate", + "acm:ListCertificates" + ], + "Resource": "*" + } + ] +} +``` + + + `RequestCertificate` cannot be scoped below `"*"` because the certificate ARN does not exist until after the call succeeds. + + +**Route 53 connection** — needs the following on your hosted zone so Infisical can write the ACM validation CNAME records: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:GetHostedZone", + "route53:ChangeResourceRecordSets" + ], + "Resource": "arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID" + } + ] +} +``` + +## Setup + + + + In the AWS Console, navigate to **Route 53 → Hosted zones** and select the public hosted zone for the domain(s) you will issue certificates for. Copy the **Hosted Zone ID** from the details panel. + ![Copy Hosted Zone ID](/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-copy-hosted-zone-id.png) + + + + In your Infisical project, go to **Certificate Authorities** and scroll to the **External Certificate Authorities** section. + ![External CA Page](/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-page.png) + + + + Click **Create CA** and configure: + - **CA Type**: **AWS ACM Public CA** + - **Name**: lowercase letters, numbers, and hyphens + - **AWS Connection**: the connection with ACM permissions + - **Route 53 Connection**: the connection with Route 53 permissions (can be the same as above) + - **Hosted Zone ID**: the Route 53 public hosted zone ID from the previous step + - **Region**: the ACM region to issue from + + ![External CA Form](/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-form.png) + + + + Create a certificate profile linked to this CA, then submit a certificate request. Infisical requests the certificate from ACM, writes the required CNAME(s) to Route 53, waits for ACM to finish validation, and saves the certificate and private key. + ![Certificate Created](/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-certificate-created.png) + + + +## Auto-Renewal + +ACM certificates expire after 198 days. There are two renewal paths that both end up producing a fresh certificate in Infisical. + +### AWS managed renewal + +AWS attempts to automatically renew ACM public certificates 45 days before expiry. This is [ACM managed renewal](https://docs.aws.amazon.com/acm/latest/userguide/managed-renewal.html) and happens on AWS's schedule, independent of Infisical. When it succeeds, AWS issues a new certificate body and private key under the **same ARN** but with a new serial number. + +The catch: AWS only updates the copy held inside ACM. The copy saved in Infisical still holds the old material until something pulls the new version out. + +### Infisical auto-renewal + +To keep the copy stored in Infisical in sync with AWS, enable auto-renewal on the certificate profile when you create or edit it: + +- **Auto-renew**: enabled +- **Renew before days**: how many days before expiry renewal should fire (1–30 days) + +Every certificate issued through that profile inherits these values. You can also override them on an individual certificate from **Certificates → Manage Renewal**. + +When a certificate reaches the configured threshold, Infisical reconciles it with AWS: + +- If AWS has **already renewed** the certificate on its own, Infisical pulls in the new certificate and private key. +- If AWS has **not yet renewed**, Infisical triggers renewal, waits for ACM to finish re-issuance, then saves the new material. + +In both cases the renewed certificate is stored as a new entry linked to the original and inherits the same auto-renewal settings — so the cycle continues automatically. + + + AWS generates a fresh private key on every renewal. Infisical pulls it in each time and stores it encrypted with your project's KMS key. + + +## Troubleshooting + +**`Failed to reach AWS Certificate Manager`** — the ACM connection credentials are invalid or missing the IAM permissions above. + +**`Failed to access Route 53 hosted zone`** — the Route 53 connection cannot read the hosted zone, or the Hosted Zone ID is wrong. Check `route53:GetHostedZone` and that the zone is public. + +**Request stays pending** — DNS validation can take several minutes. Infisical retries automatically. Verify the CNAME records exist in Route 53 and that the hosted zone is authoritative for the requested domain. + +**Renewal appears stuck** — immediately after renewal is triggered, ACM may not yet have the new certificate available. Infisical treats this as transient and retries until a new serial number appears on the ARN. + +## FAQ + + + + AWS issues every ACM public certificate with a fixed 198-day validity. + + + + Only Amazon Route 53. Infisical writes the required CNAMEs through your Route 53 connection. + + + + No. ACM generates the key pair itself. Infisical pulls the certificate and private key from ACM and stores them encrypted. + + + + `RSA_2048`, `EC_prime256v1` (ECDSA P-256), and `EC_secp384r1` (ECDSA P-384). + + + + No. ACM does not accept subject fields beyond the common name (O, OU, C, ST, L are ignored). + + + + No. ACM applies its own policy on every issued certificate. + + + + Certificate profiles backed by AWS ACM Public CA support only **API** enrollment. EST, SCEP, and ACME rely on submitting a CSR for the CA to sign, but ACM generates the private key itself and does not accept a CSR. + + + + No. ACM Public CA only issues end-entity (leaf) certificates. + + diff --git a/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-certificate-created.png b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-certificate-created.png new file mode 100644 index 00000000000..0d7fd1cf6db Binary files /dev/null and b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-certificate-created.png differ diff --git a/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-copy-hosted-zone-id.png b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-copy-hosted-zone-id.png new file mode 100644 index 00000000000..2a53da8ce82 Binary files /dev/null and b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-copy-hosted-zone-id.png differ diff --git a/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-form.png b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-form.png new file mode 100644 index 00000000000..5d1da518222 Binary files /dev/null and b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-form.png differ diff --git a/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-page.png b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-page.png new file mode 100644 index 00000000000..640aecf55a6 Binary files /dev/null and b/docs/images/platform/pki/aws-acm-public-ca/aws-acm-public-ca-external-ca-page.png differ diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index acbdc78a4ed..6ad1e305e01 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -331,6 +331,67 @@ Infisical supports two methods for connecting to AWS. Using a specific CA ARN in `Resource` is recommended over `"*"` to follow the principle of least privilege. + + Use the following custom policy to grant the minimum permissions required by Infisical to issue publicly-trusted certificates via AWS Certificate Manager and perform DNS validation through Route 53. + + **ACM permissions** — `RequestCertificate` cannot be scoped below `"*"` because the certificate ARN does not exist until after the call succeeds: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAcmPublicCaAccess", + "Effect": "Allow", + "Action": [ + "acm:RequestCertificate", + "acm:DescribeCertificate", + "acm:ExportCertificate", + "acm:RenewCertificate", + "acm:RevokeCertificate", + "acm:ListCertificates" + ], + "Resource": "*" + } + ] + } + ``` + + **Route 53 permissions** — scope to the hosted zone(s) used for DNS validation: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowRoute53ForAcmValidation", + "Effect": "Allow", + "Action": [ + "route53:GetHostedZone", + "route53:ChangeResourceRecordSets" + ], + "Resource": "arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID" + } + ] + } + ``` + + ACM and Route 53 permissions can live on the same IAM principal, or be split across two separate connections in Infisical. + + + **ACM Permissions:** + - **RequestCertificate**: Requests a new public certificate from ACM + - **DescribeCertificate**: Retrieves certificate status and DNS validation records + - **ExportCertificate**: Exports the issued certificate and private key to Infisical + - **RenewCertificate**: Triggers renewal of an existing certificate + - **RevokeCertificate**: Revokes a previously issued certificate + - **ListCertificates**: Used during connection validation + + **Route 53 Permissions:** + - **GetHostedZone**: Validates the hosted zone during CA setup + - **ChangeResourceRecordSets**: Writes the ACM DNS validation CNAME records + + @@ -662,6 +723,67 @@ Infisical supports two methods for connecting to AWS. Using a specific CA ARN in `Resource` is recommended over `"*"` to follow the principle of least privilege. + + Use the following custom policy to grant the minimum permissions required by Infisical to issue publicly-trusted certificates via AWS Certificate Manager and perform DNS validation through Route 53. + + **ACM permissions** — `RequestCertificate` cannot be scoped below `"*"` because the certificate ARN does not exist until after the call succeeds: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAcmPublicCaAccess", + "Effect": "Allow", + "Action": [ + "acm:RequestCertificate", + "acm:DescribeCertificate", + "acm:ExportCertificate", + "acm:RenewCertificate", + "acm:RevokeCertificate", + "acm:ListCertificates" + ], + "Resource": "*" + } + ] + } + ``` + + **Route 53 permissions** — scope to the hosted zone(s) used for DNS validation: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowRoute53ForAcmValidation", + "Effect": "Allow", + "Action": [ + "route53:GetHostedZone", + "route53:ChangeResourceRecordSets" + ], + "Resource": "arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID" + } + ] + } + ``` + + ACM and Route 53 permissions can live on the same IAM principal, or be split across two separate connections in Infisical. + + + **ACM Permissions:** + - **RequestCertificate**: Requests a new public certificate from ACM + - **DescribeCertificate**: Retrieves certificate status and DNS validation records + - **ExportCertificate**: Exports the issued certificate and private key to Infisical + - **RenewCertificate**: Triggers renewal of an existing certificate + - **RevokeCertificate**: Revokes a previously issued certificate + - **ListCertificates**: Used during connection validation + + **Route 53 Permissions:** + - **GetHostedZone**: Validates the hosted zone during CA setup + - **ChangeResourceRecordSets**: Writes the ACM DNS validation CNAME records + + diff --git a/frontend/src/hooks/api/ca/constants.tsx b/frontend/src/hooks/api/ca/constants.tsx index 5ab6ccad43e..17a546b7a0d 100644 --- a/frontend/src/hooks/api/ca/constants.tsx +++ b/frontend/src/hooks/api/ca/constants.tsx @@ -44,13 +44,19 @@ export const CA_TYPE_CAPABILITIES_MAP: Record = { CaCapability.ISSUE_CERTIFICATES, CaCapability.REVOKE_CERTIFICATES, CaCapability.RENEW_CERTIFICATES + ], + [CaType.AWS_ACM_PUBLIC_CA]: [ + CaCapability.ISSUE_CERTIFICATES, + CaCapability.REVOKE_CERTIFICATES, + CaCapability.RENEW_CERTIFICATES ] }; export const EXTERNAL_CA_TYPE_NAME_MAP: Record = { [CaType.ACME]: "ACME", [CaType.AZURE_AD_CS]: "Active Directory Certificate Services (AD CS)", - [CaType.AWS_PCA]: "AWS Private CA (PCA)" + [CaType.AWS_PCA]: "AWS Private CA (PCA)", + [CaType.AWS_ACM_PUBLIC_CA]: "AWS ACM Public CA" }; /** diff --git a/frontend/src/hooks/api/ca/enums.tsx b/frontend/src/hooks/api/ca/enums.tsx index 27dca00c041..872e36c5316 100644 --- a/frontend/src/hooks/api/ca/enums.tsx +++ b/frontend/src/hooks/api/ca/enums.tsx @@ -2,7 +2,8 @@ export enum CaType { INTERNAL = "internal", ACME = "acme", AZURE_AD_CS = "azure-ad-cs", - AWS_PCA = "aws-pca" + AWS_PCA = "aws-pca", + AWS_ACM_PUBLIC_CA = "aws-acm-public-ca" } export enum InternalCaType { diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index d441eca5d42..a9f5c27c746 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -82,17 +82,21 @@ export const useListExternalCasByProjectId = (projectId: string) => { return useQuery({ queryKey: caKeys.listExternalCasByProjectId(projectId), queryFn: async () => { - const [acmeResponse, azureAdCsResponse, awsPcaResponse] = await Promise.allSettled([ - apiRequest.get( - `/api/v1/cert-manager/ca/${CaType.ACME}?projectId=${projectId}` - ), - apiRequest.get( - `/api/v1/cert-manager/ca/${CaType.AZURE_AD_CS}?projectId=${projectId}` - ), - apiRequest.get( - `/api/v1/cert-manager/ca/${CaType.AWS_PCA}?projectId=${projectId}` - ) - ]); + const [acmeResponse, azureAdCsResponse, awsPcaResponse, awsAcmPublicCaResponse] = + await Promise.allSettled([ + apiRequest.get( + `/api/v1/cert-manager/ca/${CaType.ACME}?projectId=${projectId}` + ), + apiRequest.get( + `/api/v1/cert-manager/ca/${CaType.AZURE_AD_CS}?projectId=${projectId}` + ), + apiRequest.get( + `/api/v1/cert-manager/ca/${CaType.AWS_PCA}?projectId=${projectId}` + ), + apiRequest.get( + `/api/v1/cert-manager/ca/${CaType.AWS_ACM_PUBLIC_CA}?projectId=${projectId}` + ) + ]); const allCas: TUnifiedCertificateAuthority[] = []; @@ -108,6 +112,10 @@ export const useListExternalCasByProjectId = (projectId: string) => { allCas.push(...awsPcaResponse.value.data); } + if (awsAcmPublicCaResponse.status === "fulfilled") { + allCas.push(...awsAcmPublicCaResponse.value.data); + } + return allCas; } }); diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 5614d403b08..c0e7e618509 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -49,6 +49,21 @@ export type TAwsPcaCertificateAuthority = { }; }; +export type TAwsAcmPublicCaCertificateAuthority = { + id: string; + projectId: string; + type: CaType.AWS_ACM_PUBLIC_CA; + status: CaStatus; + name: string; + enableDirectIssuance: boolean; + configuration: { + appConnectionId: string; + dnsAppConnectionId: string; + hostedZoneId: string; + region: string; + }; +}; + export type TInternalCertificateAuthority = { id: string; projectId: string; @@ -80,6 +95,7 @@ export type TUnifiedCertificateAuthority = | TAcmeCertificateAuthority | TAzureAdCsCertificateAuthority | TAwsPcaCertificateAuthority + | TAwsAcmPublicCaCertificateAuthority | TInternalCertificateAuthority; export type TCreateCertificateAuthorityDTO = Omit< diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx index 0595322368d..38899aecf69 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx @@ -125,6 +125,19 @@ const awsPcaConfigurationSchema = z.object({ region: z.string().min(1, "Region is required") }); +const awsAcmPublicCaConfigurationSchema = z.object({ + awsConnection: z.object({ + id: z.string().min(1, "AWS Connection is required"), + name: z.string() + }), + dnsConnection: z.object({ + id: z.string().min(1, "Route 53 Connection is required"), + name: z.string() + }), + hostedZoneId: z.string().trim().min(1, "Hosted Zone ID is required"), + region: z.string().min(1, "Region is required") +}); + const schema = z.discriminatedUnion("type", [ baseSchema.extend({ type: z.literal(CaType.ACME), @@ -137,6 +150,10 @@ const schema = z.discriminatedUnion("type", [ baseSchema.extend({ type: z.literal(CaType.AWS_PCA), configuration: awsPcaConfigurationSchema + }), + baseSchema.extend({ + type: z.literal(CaType.AWS_ACM_PUBLIC_CA), + configuration: awsAcmPublicCaConfigurationSchema }) ]); @@ -150,7 +167,8 @@ type Props = { const caTypes = [ { label: "ACME", value: CaType.ACME }, { label: "Active Directory Certificate Services (AD CS)", value: CaType.AZURE_AD_CS }, - { label: "AWS Private CA (PCA)", value: CaType.AWS_PCA } + { label: "AWS Private CA (PCA)", value: CaType.AWS_PCA }, + { label: "AWS ACM Public CA", value: CaType.AWS_ACM_PUBLIC_CA } ]; export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { @@ -214,6 +232,24 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { region: "" } }); + } else if (initialType === CaType.AWS_ACM_PUBLIC_CA) { + reset({ + type: CaType.AWS_ACM_PUBLIC_CA, + name: "", + status: CaStatus.ACTIVE, + configuration: { + awsConnection: { + id: "", + name: "" + }, + dnsConnection: { + id: "", + name: "" + }, + hostedZoneId: "", + region: "" + } + }); } else { reset({ type: CaType.ACME, @@ -268,7 +304,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { AppConnection.AWS, currentProject.id, { - enabled: caType === CaType.AWS_PCA + enabled: caType === CaType.AWS_PCA || caType === CaType.AWS_ACM_PUBLIC_CA } ); @@ -276,7 +312,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { if (caType === CaType.AZURE_AD_CS) { return availableAzureConnections || []; } - if (caType === CaType.AWS_PCA) { + if (caType === CaType.AWS_PCA || caType === CaType.AWS_ACM_PUBLIC_CA) { return availableAwsConnections || []; } return [ @@ -301,7 +337,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { isDNSMadeEasyPending || isAzureDNSPending || (isAzurePending && caType === CaType.AZURE_AD_CS) || - (isAwsPending && caType === CaType.AWS_PCA); + (isAwsPending && (caType === CaType.AWS_PCA || caType === CaType.AWS_ACM_PUBLIC_CA)); const dnsAppConnection = caType === CaType.ACME && configuration && "dnsAppConnection" in configuration @@ -385,6 +421,35 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { region: ca.configuration.region } }); + } else if (ca.type === CaType.AWS_ACM_PUBLIC_CA && availableConnections?.length) { + const selectedConnection = availableConnections?.find( + (connection) => connection.id === ca.configuration.appConnectionId + ); + const selectedDnsConnection = ca.configuration.dnsAppConnectionId + ? availableConnections?.find( + (connection) => connection.id === ca.configuration.dnsAppConnectionId + ) + : undefined; + + reset({ + type: ca.type, + name: ca.name, + status: ca.status, + configuration: { + awsConnection: { + id: ca.configuration.appConnectionId, + name: selectedConnection?.name || "" + }, + dnsConnection: ca.configuration.dnsAppConnectionId + ? { + id: ca.configuration.dnsAppConnectionId, + name: selectedDnsConnection?.name || "" + } + : undefined, + hostedZoneId: ca.configuration.hostedZoneId || "", + region: ca.configuration.region + } + }); } } }, [ca, availableConnections, reset, isCaLoading]); @@ -419,6 +484,13 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { certificateAuthorityArn: formConfiguration.certificateAuthorityArn, region: formConfiguration.region }; + } else if (type === CaType.AWS_ACM_PUBLIC_CA && "awsConnection" in formConfiguration) { + configPayload = { + appConnectionId: formConfiguration.awsConnection.id, + dnsAppConnectionId: formConfiguration.dnsConnection.id, + hostedZoneId: formConfiguration.hostedZoneId, + region: formConfiguration.region + }; } else { throw new Error("Invalid certificate authority configuration"); } @@ -834,6 +906,93 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { /> )} + {caType === CaType.AWS_ACM_PUBLIC_CA && ( + <> + ( + + { + onChange(newValue); + }} + isLoading={isPending} + options={availableConnections} + placeholder="Select connection..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} + /> + + )} + control={control} + name="configuration.awsConnection" + /> + ( + + { + onChange(newValue); + }} + isLoading={isPending} + options={availableConnections} + placeholder="Select connection..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} + /> + + )} + control={control} + name="configuration.dnsConnection" + /> + ( + + + + )} + /> + ( + + onChange(v || "")} /> + + )} + /> + + )}