@jaypie/constructs 1.2.8 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,6 @@
3
3
  var cdk = require('aws-cdk-lib');
4
4
  var s3 = require('aws-cdk-lib/aws-s3');
5
5
  var constructs = require('constructs');
6
- var acm = require('aws-cdk-lib/aws-certificatemanager');
7
6
  var apiGateway = require('aws-cdk-lib/aws-apigateway');
8
7
  var route53 = require('aws-cdk-lib/aws-route53');
9
8
  var route53Targets = require('aws-cdk-lib/aws-route53-targets');
@@ -11,6 +10,7 @@ var secretsmanager = require('aws-cdk-lib/aws-secretsmanager');
11
10
  var datadogCdkConstructsV2 = require('datadog-cdk-constructs-v2');
12
11
  var errors = require('@jaypie/errors');
13
12
  var awsIam = require('aws-cdk-lib/aws-iam');
13
+ var acm = require('aws-cdk-lib/aws-certificatemanager');
14
14
  var lambda = require('aws-cdk-lib/aws-lambda');
15
15
  var logDestinations = require('aws-cdk-lib/aws-logs-destinations');
16
16
  var s3n = require('aws-cdk-lib/aws-s3-notifications');
@@ -47,11 +47,11 @@ function _interopNamespaceDefault(e) {
47
47
 
48
48
  var cdk__namespace = /*#__PURE__*/_interopNamespaceDefault(cdk);
49
49
  var s3__namespace = /*#__PURE__*/_interopNamespaceDefault(s3);
50
- var acm__namespace = /*#__PURE__*/_interopNamespaceDefault(acm);
51
50
  var apiGateway__namespace = /*#__PURE__*/_interopNamespaceDefault(apiGateway);
52
51
  var route53__namespace = /*#__PURE__*/_interopNamespaceDefault(route53);
53
52
  var route53Targets__namespace = /*#__PURE__*/_interopNamespaceDefault(route53Targets);
54
53
  var secretsmanager__namespace = /*#__PURE__*/_interopNamespaceDefault(secretsmanager);
54
+ var acm__namespace = /*#__PURE__*/_interopNamespaceDefault(acm);
55
55
  var lambda__namespace = /*#__PURE__*/_interopNamespaceDefault(lambda);
56
56
  var logDestinations__namespace = /*#__PURE__*/_interopNamespaceDefault(logDestinations);
57
57
  var s3n__namespace = /*#__PURE__*/_interopNamespaceDefault(s3n);
@@ -135,7 +135,7 @@ const CDK$2 = {
135
135
  },
136
136
  DURATION: {
137
137
  EXPRESS_API: 30,
138
- CLOUDFRONT_API: 180,
138
+ CLOUDFRONT_API: 120,
139
139
  LAMBDA_MAXIMUM: 900,
140
140
  LAMBDA_WORKER: 900,
141
141
  },
@@ -476,6 +476,111 @@ function extendDatadogRole(scope, options) {
476
476
  return datadogCustomPolicy;
477
477
  }
478
478
 
479
+ // Cache: Stack -> (domain -> certificate)
480
+ // Using WeakMap for automatic garbage collection when stacks are destroyed
481
+ const certificateCache = new WeakMap();
482
+ /**
483
+ * Resolves a certificate based on input type.
484
+ *
485
+ * Key behavior: When certificate is `true`, the certificate is created at the
486
+ * STACK level (not construct level) and cached by domain name. This allows
487
+ * swapping between constructs (e.g., JaypieDistribution to JaypieApiGateway)
488
+ * without recreating the certificate.
489
+ *
490
+ * @param scope - The construct scope (used to find the stack)
491
+ * @param options - Certificate resolution options
492
+ * @returns The resolved certificate, or undefined if certificate is false
493
+ *
494
+ * @example
495
+ * // Create or get cached certificate at stack level
496
+ * const cert = resolveCertificate(this, {
497
+ * certificate: true,
498
+ * domainName: "api.example.com",
499
+ * zone: hostedZone,
500
+ * });
501
+ *
502
+ * @example
503
+ * // Use existing certificate
504
+ * const cert = resolveCertificate(this, {
505
+ * certificate: existingCert,
506
+ * domainName: "api.example.com",
507
+ * zone: hostedZone,
508
+ * });
509
+ *
510
+ * @example
511
+ * // Import certificate from ARN
512
+ * const cert = resolveCertificate(this, {
513
+ * certificate: "arn:aws:acm:us-east-1:123456789:certificate/abc-123",
514
+ * domainName: "api.example.com",
515
+ * zone: hostedZone,
516
+ * });
517
+ */
518
+ function resolveCertificate(scope, options) {
519
+ const { certificate, domainName, name = "Certificate", roleTag = CDK$2.ROLE.API, zone, } = options;
520
+ // false = no certificate
521
+ if (certificate === false) {
522
+ return undefined;
523
+ }
524
+ // ICertificate passed directly - use as-is
525
+ if (typeof certificate === "object" && certificate !== null) {
526
+ return certificate;
527
+ }
528
+ // ARN string - import from ARN
529
+ if (typeof certificate === "string") {
530
+ // Sanitize domain for construct ID
531
+ const sanitizedDomain = sanitizeDomainForId(domainName);
532
+ return acm__namespace.Certificate.fromCertificateArn(scope, `${name}-${sanitizedDomain}`, certificate);
533
+ }
534
+ // true (default) = create at STACK level with caching
535
+ const stack = cdk.Stack.of(scope);
536
+ // Get or create cache for this stack
537
+ let stackCache = certificateCache.get(stack);
538
+ if (!stackCache) {
539
+ stackCache = new Map();
540
+ certificateCache.set(stack, stackCache);
541
+ }
542
+ // Return cached certificate if one exists for this domain
543
+ const cached = stackCache.get(domainName);
544
+ if (cached) {
545
+ return cached;
546
+ }
547
+ // Create certificate at STACK level (not construct level!)
548
+ // This is the key difference - the certificate's lifecycle is tied to the stack,
549
+ // not to the individual construct that requested it
550
+ const sanitizedDomain = sanitizeDomainForId(domainName);
551
+ const cert = new acm__namespace.Certificate(stack, `${name}-${sanitizedDomain}`, {
552
+ domainName,
553
+ validation: acm__namespace.CertificateValidation.fromDns(zone),
554
+ });
555
+ cdk.Tags.of(cert).add(CDK$2.TAG.ROLE, roleTag);
556
+ // Cache for future requests
557
+ stackCache.set(domainName, cert);
558
+ return cert;
559
+ }
560
+ /**
561
+ * Sanitizes a domain name for use in CDK construct IDs.
562
+ * CDK construct IDs can only contain alphanumeric characters and hyphens.
563
+ */
564
+ function sanitizeDomainForId(domain) {
565
+ return domain.replace(/\./g, "-").replace(/[^a-zA-Z0-9-]/g, "");
566
+ }
567
+ /**
568
+ * Clears the certificate cache for a specific stack.
569
+ * Primarily useful for testing.
570
+ */
571
+ function clearCertificateCache(stack) {
572
+ certificateCache.delete(stack);
573
+ }
574
+ /**
575
+ * Clears all certificate caches.
576
+ * Primarily useful for testing.
577
+ */
578
+ function clearAllCertificateCaches() {
579
+ // WeakMap doesn't have a clear() method, so we create a new one
580
+ // This is a no-op since we can't actually clear a WeakMap,
581
+ // but stacks going out of scope will be garbage collected anyway
582
+ }
583
+
479
584
  /**
480
585
  * Check if the current environment matches the given environment
481
586
  */
@@ -805,34 +910,34 @@ const resolveParamsAndSecrets = ({ paramsAndSecrets, options, } = {}) => {
805
910
  };
806
911
 
807
912
  // It is a consumer if the environment is ephemeral
808
- function checkEnvIsConsumer(env = process.env) {
913
+ function checkEnvIsConsumer$1(env = process.env) {
809
914
  return (env.PROJECT_ENV === CDK$2.ENV.PERSONAL ||
810
915
  !!env.CDK_ENV_PERSONAL ||
811
916
  /** @deprecated */ env.PROJECT_ENV === "ephemeral" ||
812
917
  /** @deprecated */ !!env.CDK_ENV_EPHEMERAL);
813
918
  }
814
- function checkEnvIsProvider(env = process.env) {
919
+ function checkEnvIsProvider$1(env = process.env) {
815
920
  return env.PROJECT_ENV === CDK$2.ENV.SANDBOX;
816
921
  }
817
- function cleanName(name) {
922
+ function cleanName$1(name) {
818
923
  return name.replace(/[^a-zA-Z0-9:-]/g, "");
819
924
  }
820
- function exportEnvName(name, env = process.env) {
925
+ function exportEnvName$1(name, env = process.env) {
821
926
  let rawName;
822
- if (checkEnvIsProvider(env)) {
927
+ if (checkEnvIsProvider$1(env)) {
823
928
  rawName = `env-${env.PROJECT_ENV}-${env.PROJECT_KEY}-${name}`;
824
929
  // Clean the entire name to only allow alphanumeric, colons, and hyphens
825
- return cleanName(rawName);
930
+ return cleanName$1(rawName);
826
931
  }
827
932
  else {
828
- if (checkEnvIsConsumer(env)) {
933
+ if (checkEnvIsConsumer$1(env)) {
829
934
  rawName = `env-${CDK$2.ENV.SANDBOX}-${env.PROJECT_KEY}-${name}`;
830
935
  }
831
936
  else {
832
937
  rawName = `env-${env.PROJECT_ENV}-${env.PROJECT_KEY}-${name}`;
833
938
  }
834
939
  }
835
- return cleanName(rawName);
940
+ return cleanName$1(rawName);
836
941
  }
837
942
  class JaypieEnvSecret extends constructs.Construct {
838
943
  constructor(scope, idOrEnvKey, props) {
@@ -844,15 +949,15 @@ class JaypieEnvSecret extends constructs.Construct {
844
949
  process.env[idOrEnvKey] !== "";
845
950
  const id = treatAsEnvKey ? `EnvSecret_${idOrEnvKey}` : idOrEnvKey;
846
951
  super(scope, id);
847
- const { consumer = checkEnvIsConsumer(), envKey: envKeyProp, export: exportParam, generateSecretString, provider = checkEnvIsProvider(), roleTag, vendorTag, value, } = props || {};
952
+ const { consumer = checkEnvIsConsumer$1(), envKey: envKeyProp, export: exportParam, generateSecretString, provider = checkEnvIsProvider$1(), roleTag, vendorTag, value, } = props || {};
848
953
  const envKey = treatAsEnvKey ? idOrEnvKey : envKeyProp;
849
954
  this._envKey = envKey;
850
955
  let exportName;
851
956
  if (!exportParam) {
852
- exportName = exportEnvName(id);
957
+ exportName = exportEnvName$1(id);
853
958
  }
854
959
  else {
855
- exportName = cleanName(exportParam);
960
+ exportName = cleanName$1(exportParam);
856
961
  }
857
962
  if (consumer) {
858
963
  const secretName = cdk.Fn.importValue(exportName);
@@ -1065,22 +1170,18 @@ class JaypieApiGateway extends constructs.Construct {
1065
1170
  }
1066
1171
  }
1067
1172
  const apiGatewayName = name || constructEnvName("ApiGateway");
1068
- const certificateName = constructEnvName("Certificate");
1069
1173
  const apiDomainName = constructEnvName("ApiDomainName");
1070
1174
  let hostedZone;
1071
1175
  let certificateToUse;
1072
1176
  if (host && zone) {
1073
1177
  hostedZone = resolveHostedZone(this, { zone });
1074
- if (certificate === true) {
1075
- certificateToUse = new acm__namespace.Certificate(this, certificateName, {
1076
- domainName: host,
1077
- validation: acm__namespace.CertificateValidation.fromDns(hostedZone),
1078
- });
1079
- cdk.Tags.of(certificateToUse).add(CDK$2.TAG.ROLE, CDK$2.ROLE.HOSTING);
1080
- }
1081
- else if (typeof certificate === "object") {
1082
- certificateToUse = certificate;
1083
- }
1178
+ // Use resolveCertificate to create certificate at stack level (enables reuse when swapping constructs)
1179
+ certificateToUse = resolveCertificate(this, {
1180
+ certificate,
1181
+ domainName: host,
1182
+ roleTag: CDK$2.ROLE.HOSTING,
1183
+ zone: hostedZone,
1184
+ });
1084
1185
  this._certificate = certificateToUse;
1085
1186
  this._host = host;
1086
1187
  }
@@ -1840,6 +1941,238 @@ class JaypieBucketQueuedLambda extends JaypieQueuedLambda {
1840
1941
  }
1841
1942
  }
1842
1943
 
1944
+ // Check if environment is a consumer (personal/ephemeral builds import from sandbox)
1945
+ function checkEnvIsConsumer(env = process.env) {
1946
+ return (env.PROJECT_ENV === CDK$2.ENV.PERSONAL ||
1947
+ !!env.CDK_ENV_PERSONAL ||
1948
+ env.PROJECT_ENV === "ephemeral" ||
1949
+ !!env.CDK_ENV_EPHEMERAL);
1950
+ }
1951
+ // Check if environment is a provider (sandbox exports for consumers)
1952
+ function checkEnvIsProvider(env = process.env) {
1953
+ return env.PROJECT_ENV === CDK$2.ENV.SANDBOX;
1954
+ }
1955
+ // Sanitize export name to only allow alphanumeric, colons, and hyphens
1956
+ function cleanName(name) {
1957
+ return name.replace(/[^a-zA-Z0-9:-]/g, "");
1958
+ }
1959
+ // Generate export name based on environment
1960
+ function exportEnvName(name, env = process.env) {
1961
+ const projectKey = env.PROJECT_KEY || "default";
1962
+ let rawName;
1963
+ if (checkEnvIsProvider(env)) {
1964
+ rawName = `env-${env.PROJECT_ENV}-${projectKey}-cert-${name}`;
1965
+ }
1966
+ else if (checkEnvIsConsumer(env)) {
1967
+ rawName = `env-${CDK$2.ENV.SANDBOX}-${projectKey}-cert-${name}`;
1968
+ }
1969
+ else {
1970
+ rawName = `env-${env.PROJECT_ENV || "default"}-${projectKey}-cert-${name}`;
1971
+ }
1972
+ return cleanName(rawName);
1973
+ }
1974
+ // Resolve domain name from props or environment (called before super)
1975
+ function resolveDomainNameFromProps(props) {
1976
+ if (props?.domainName) {
1977
+ return props.domainName;
1978
+ }
1979
+ if (process.env.CDK_ENV_API_HOST_NAME) {
1980
+ return process.env.CDK_ENV_API_HOST_NAME;
1981
+ }
1982
+ if (process.env.CDK_ENV_API_SUBDOMAIN) {
1983
+ return mergeDomain(process.env.CDK_ENV_API_SUBDOMAIN, process.env.CDK_ENV_API_HOSTED_ZONE ||
1984
+ process.env.CDK_ENV_HOSTED_ZONE ||
1985
+ "");
1986
+ }
1987
+ return undefined;
1988
+ }
1989
+ // Sanitize domain for construct ID
1990
+ function sanitizeDomain(domain) {
1991
+ return domain.replace(/\./g, "-");
1992
+ }
1993
+ /**
1994
+ * A standalone certificate construct that can be shared across constructs.
1995
+ *
1996
+ * Key feature: Uses the same `resolveCertificate()` helper as JaypieDistribution,
1997
+ * JaypieApiGateway, etc. This means:
1998
+ * - Certificates are created at the stack level and cached by domain
1999
+ * - You can "take over" a certificate from another construct by using the same domain
2000
+ * - Swapping between JaypieDistribution and JaypieApiGateway won't recreate certs
2001
+ *
2002
+ * Supports flexible constructor signatures:
2003
+ * - `new JaypieCertificate(scope)` - uses environment defaults
2004
+ * - `new JaypieCertificate(scope, props)` - ID auto-generated from domain
2005
+ * - `new JaypieCertificate(scope, id, props)` - explicit ID
2006
+ *
2007
+ * @example
2008
+ * // Minimal - uses environment variables for domain/zone
2009
+ * const cert = new JaypieCertificate(this);
2010
+ *
2011
+ * @example
2012
+ * // With options - ID auto-generated as "JaypieCert-api-example-com"
2013
+ * const cert = new JaypieCertificate(this, {
2014
+ * domainName: "api.example.com",
2015
+ * zone: "example.com",
2016
+ * });
2017
+ *
2018
+ * @example
2019
+ * // Explicit ID - useful when you need a specific construct ID
2020
+ * const cert = new JaypieCertificate(this, "MyApiCert", {
2021
+ * domainName: "api.example.com",
2022
+ * zone: "example.com",
2023
+ * });
2024
+ *
2025
+ * @example
2026
+ * // Take over from JaypieDistribution (uses same ID format)
2027
+ * // After removing JaypieDistribution with certificate: true
2028
+ * const cert = new JaypieCertificate(this, {
2029
+ * domainName: "api.example.com",
2030
+ * zone: "example.com",
2031
+ * });
2032
+ *
2033
+ * @example
2034
+ * // Provider/consumer pattern for cross-stack sharing
2035
+ * // In sandbox stack:
2036
+ * new JaypieCertificate(this, { provider: true });
2037
+ *
2038
+ * // In personal build:
2039
+ * new JaypieCertificate(this, { consumer: true });
2040
+ */
2041
+ class JaypieCertificate extends constructs.Construct {
2042
+ constructor(scope, idOrProps, maybeProps) {
2043
+ // Resolve constructor arguments
2044
+ let id;
2045
+ let props;
2046
+ if (typeof idOrProps === "string") {
2047
+ // (scope, id, props) pattern
2048
+ id = idOrProps;
2049
+ props = maybeProps || {};
2050
+ }
2051
+ else if (typeof idOrProps === "object" && idOrProps !== null) {
2052
+ // (scope, props) pattern - auto-generate id
2053
+ props = idOrProps;
2054
+ const domainName = resolveDomainNameFromProps(props);
2055
+ if (domainName) {
2056
+ // Use "JaypieCert-" prefix to avoid collision with internal certificate
2057
+ id = props.id || `JaypieCert-${sanitizeDomain(domainName)}`;
2058
+ }
2059
+ else {
2060
+ id = props.id || "JaypieCert";
2061
+ }
2062
+ }
2063
+ else {
2064
+ // (scope) pattern - no id, no props
2065
+ props = {};
2066
+ const domainName = resolveDomainNameFromProps(props);
2067
+ if (domainName) {
2068
+ id = `JaypieCert-${sanitizeDomain(domainName)}`;
2069
+ }
2070
+ else {
2071
+ id = "JaypieCert";
2072
+ }
2073
+ }
2074
+ super(scope, id);
2075
+ const { consumer = checkEnvIsConsumer(), domainName: propsDomainName, export: exportParam, provider = checkEnvIsProvider(), roleTag = CDK$2.ROLE.API, zone: propsZone, } = props;
2076
+ // Validate environment variables
2077
+ if (process.env.CDK_ENV_API_SUBDOMAIN &&
2078
+ !isValidSubdomain(process.env.CDK_ENV_API_SUBDOMAIN)) {
2079
+ throw new Error("CDK_ENV_API_SUBDOMAIN is not a valid subdomain");
2080
+ }
2081
+ if (process.env.CDK_ENV_API_HOSTED_ZONE &&
2082
+ !isValidHostname$1(process.env.CDK_ENV_API_HOSTED_ZONE)) {
2083
+ throw new Error("CDK_ENV_API_HOSTED_ZONE is not a valid hostname");
2084
+ }
2085
+ if (process.env.CDK_ENV_HOSTED_ZONE &&
2086
+ !isValidHostname$1(process.env.CDK_ENV_HOSTED_ZONE)) {
2087
+ throw new Error("CDK_ENV_HOSTED_ZONE is not a valid hostname");
2088
+ }
2089
+ // Determine domain name from props or environment
2090
+ let domainName = propsDomainName;
2091
+ if (!domainName) {
2092
+ if (process.env.CDK_ENV_API_HOST_NAME) {
2093
+ domainName = process.env.CDK_ENV_API_HOST_NAME;
2094
+ }
2095
+ else if (process.env.CDK_ENV_API_SUBDOMAIN) {
2096
+ domainName = mergeDomain(process.env.CDK_ENV_API_SUBDOMAIN, process.env.CDK_ENV_API_HOSTED_ZONE ||
2097
+ process.env.CDK_ENV_HOSTED_ZONE ||
2098
+ "");
2099
+ }
2100
+ }
2101
+ if (!domainName) {
2102
+ throw new Error("domainName is required for JaypieCertificate (or set CDK_ENV_API_HOST_NAME / CDK_ENV_API_SUBDOMAIN)");
2103
+ }
2104
+ if (!isValidHostname$1(domainName)) {
2105
+ throw new Error("domainName is not a valid hostname");
2106
+ }
2107
+ this.domainName = domainName;
2108
+ // Determine zone from props or environment
2109
+ const zone = propsZone ||
2110
+ process.env.CDK_ENV_API_HOSTED_ZONE ||
2111
+ process.env.CDK_ENV_HOSTED_ZONE;
2112
+ // Generate export name
2113
+ const sanitizedDomain = domainName.replace(/\./g, "-");
2114
+ const exportName = exportParam
2115
+ ? cleanName(exportParam)
2116
+ : exportEnvName(sanitizedDomain);
2117
+ if (consumer) {
2118
+ // Import certificate ARN from provider stack
2119
+ const certificateArn = cdk.Fn.importValue(exportName);
2120
+ this.certificate = acm__namespace.Certificate.fromCertificateArn(this, "ImportedCertificate", certificateArn);
2121
+ this.certificateArn = certificateArn;
2122
+ new cdk.CfnOutput(this, "ConsumedCertificateArn", {
2123
+ value: this.certificateArn,
2124
+ });
2125
+ }
2126
+ else {
2127
+ // Create or get cached certificate at stack level
2128
+ if (!zone) {
2129
+ throw new Error("zone is required for JaypieCertificate when not consuming (or set CDK_ENV_API_HOSTED_ZONE / CDK_ENV_HOSTED_ZONE)");
2130
+ }
2131
+ const hostedZone = resolveHostedZone(this, { zone });
2132
+ // Use resolveCertificate to create at stack level (enables sharing)
2133
+ const cert = resolveCertificate(this, {
2134
+ certificate: true,
2135
+ domainName,
2136
+ roleTag,
2137
+ zone: hostedZone,
2138
+ });
2139
+ if (!cert) {
2140
+ throw new Error("Failed to create certificate");
2141
+ }
2142
+ this.certificate = cert;
2143
+ this.certificateArn = cert.certificateArn;
2144
+ if (provider) {
2145
+ new cdk.CfnOutput(this, "ProvidedCertificateArn", {
2146
+ value: this.certificateArn,
2147
+ exportName,
2148
+ });
2149
+ }
2150
+ else {
2151
+ new cdk.CfnOutput(this, "CertificateArn", {
2152
+ value: this.certificateArn,
2153
+ });
2154
+ }
2155
+ }
2156
+ }
2157
+ // IResource implementation
2158
+ get stack() {
2159
+ return cdk.Stack.of(this);
2160
+ }
2161
+ get env() {
2162
+ return {
2163
+ account: cdk.Stack.of(this).account,
2164
+ region: cdk.Stack.of(this).region,
2165
+ };
2166
+ }
2167
+ applyRemovalPolicy(policy) {
2168
+ this.certificate.applyRemovalPolicy(policy);
2169
+ }
2170
+ // ICertificate implementation
2171
+ metricDaysToExpiry(props) {
2172
+ return this.certificate.metricDaysToExpiry(props);
2173
+ }
2174
+ }
2175
+
1843
2176
  class JaypieDatadogBucket extends constructs.Construct {
1844
2177
  /**
1845
2178
  * Create a new S3 bucket for Datadog log archiving with automatic IAM permissions
@@ -2117,16 +2450,13 @@ class JaypieDistribution extends constructs.Construct {
2117
2450
  let certificateToUse;
2118
2451
  if (host && zone && certificateProp !== false) {
2119
2452
  hostedZone = resolveHostedZone(this, { zone });
2120
- if (certificateProp === true) {
2121
- certificateToUse = new acm__namespace.Certificate(this, constructEnvName("Certificate"), {
2122
- domainName: host,
2123
- validation: acm__namespace.CertificateValidation.fromDns(hostedZone),
2124
- });
2125
- cdk.Tags.of(certificateToUse).add(CDK$2.TAG.ROLE, roleTag);
2126
- }
2127
- else if (typeof certificateProp === "object") {
2128
- certificateToUse = certificateProp;
2129
- }
2453
+ // Use resolveCertificate to create certificate at stack level (enables reuse when swapping constructs)
2454
+ certificateToUse = resolveCertificate(this, {
2455
+ certificate: certificateProp,
2456
+ domainName: host,
2457
+ roleTag,
2458
+ zone: hostedZone,
2459
+ });
2130
2460
  this.certificate = certificateToUse;
2131
2461
  }
2132
2462
  // Create log bucket if logging is enabled
@@ -3613,19 +3943,17 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
3613
3943
  else {
3614
3944
  hostedZone = zone;
3615
3945
  }
3616
- // Create certificate if not provided
3617
- if (props.certificate !== false) {
3618
- this.certificate =
3619
- typeof props.certificate === "object"
3620
- ? props.certificate
3621
- : new acm__namespace.Certificate(this, "Certificate", {
3622
- domainName: host,
3623
- validation: acm__namespace.CertificateValidation.fromDns(hostedZone),
3624
- });
3946
+ // Use resolveCertificate to create certificate at stack level (enables reuse when swapping constructs)
3947
+ this.certificate = resolveCertificate(this, {
3948
+ certificate: props.certificate,
3949
+ domainName: host,
3950
+ roleTag,
3951
+ zone: hostedZone,
3952
+ });
3953
+ if (this.certificate) {
3625
3954
  new cdk.CfnOutput(this, "CertificateArn", {
3626
3955
  value: this.certificate.certificateArn,
3627
3956
  });
3628
- cdk.Tags.of(this.certificate).add(CDK$2.TAG.ROLE, roleTag);
3629
3957
  }
3630
3958
  // Create CloudFront distribution
3631
3959
  this.distribution = new cloudfront__namespace.Distribution(this, "Distribution", {
@@ -3873,6 +4201,7 @@ exports.JaypieAccountLoggingBucket = JaypieAccountLoggingBucket;
3873
4201
  exports.JaypieApiGateway = JaypieApiGateway;
3874
4202
  exports.JaypieAppStack = JaypieAppStack;
3875
4203
  exports.JaypieBucketQueuedLambda = JaypieBucketQueuedLambda;
4204
+ exports.JaypieCertificate = JaypieCertificate;
3876
4205
  exports.JaypieDatadogBucket = JaypieDatadogBucket;
3877
4206
  exports.JaypieDatadogForwarder = JaypieDatadogForwarder;
3878
4207
  exports.JaypieDatadogSecret = JaypieDatadogSecret;
@@ -3900,7 +4229,9 @@ exports.JaypieTraceSigningKeySecret = JaypieTraceSigningKeySecret;
3900
4229
  exports.JaypieWebDeploymentBucket = JaypieWebDeploymentBucket;
3901
4230
  exports.LAMBDA_WEB_ADAPTER = LAMBDA_WEB_ADAPTER;
3902
4231
  exports.addDatadogLayers = addDatadogLayers;
4232
+ exports.clearAllCertificateCaches = clearAllCertificateCaches;
3903
4233
  exports.clearAllSecretsCaches = clearAllSecretsCaches;
4234
+ exports.clearCertificateCache = clearCertificateCache;
3904
4235
  exports.clearSecretsCache = clearSecretsCache;
3905
4236
  exports.constructEnvName = constructEnvName;
3906
4237
  exports.constructStackName = constructStackName;
@@ -3914,6 +4245,7 @@ exports.isValidHostname = isValidHostname$1;
3914
4245
  exports.isValidSubdomain = isValidSubdomain;
3915
4246
  exports.jaypieLambdaEnv = jaypieLambdaEnv;
3916
4247
  exports.mergeDomain = mergeDomain;
4248
+ exports.resolveCertificate = resolveCertificate;
3917
4249
  exports.resolveDatadogForwarderFunction = resolveDatadogForwarderFunction;
3918
4250
  exports.resolveDatadogLayers = resolveDatadogLayers;
3919
4251
  exports.resolveDatadogLoggingDestination = resolveDatadogLoggingDestination;