diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca837cb..364d1a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - name: Install dependencies run: npm install - name: build @@ -102,7 +102,7 @@ jobs: steps: - uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - name: Download build artifacts uses: actions/download-artifact@v5 with: @@ -136,7 +136,7 @@ jobs: steps: - uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 034e483..50b64f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - name: Install dependencies run: npm ci - name: release @@ -72,7 +72,7 @@ jobs: steps: - uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - name: Download build artifacts uses: actions/download-artifact@v5 with: @@ -96,7 +96,7 @@ jobs: steps: - uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - name: Download build artifacts uses: actions/download-artifact@v5 with: @@ -136,7 +136,7 @@ jobs: steps: - uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/upgrade-main.yml b/.github/workflows/upgrade-main.yml index acbde90..28c3dde 100644 --- a/.github/workflows/upgrade-main.yml +++ b/.github/workflows/upgrade-main.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v5 with: - node-version: 18.x + node-version: 24.x - name: Install dependencies run: npm ci - name: Upgrade dependencies diff --git a/.npmignore b/.npmignore index 5e8ae35..2cb5121 100644 --- a/.npmignore +++ b/.npmignore @@ -24,6 +24,10 @@ dist tsconfig.tsbuildinfo /.eslintrc.json !.jsii +/.plans/ +/cdk.context.json +/cdk.out*/ +/.env* /.gitattributes /.projenrc.ts /projenrc diff --git a/.projen/deps.json b/.projen/deps.json index 4bf821c..f5ded9a 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -2,12 +2,12 @@ "dependencies": [ { "name": "@aws-cdk/integ-runner", - "version": "2.168.0-alpha.0", + "version": "2.205.1", "type": "build" }, { "name": "@aws-cdk/integ-tests-alpha", - "version": "2.168.0-alpha.0", + "version": "2.268.0-alpha.0", "type": "build" }, { @@ -16,17 +16,17 @@ }, { "name": "@types/node", - "version": "^18", + "version": "^22", "type": "build" }, { "name": "@typescript-eslint/eslint-plugin", - "version": "^8", + "version": "^8.69.0", "type": "build" }, { "name": "@typescript-eslint/parser", - "version": "^8", + "version": "^8.69.0", "type": "build" }, { @@ -70,7 +70,7 @@ }, { "name": "jsii-docgen", - "version": "^10.5.0", + "version": "^10.12.6", "type": "build" }, { @@ -79,12 +79,12 @@ }, { "name": "jsii-rosetta", - "version": "~5.7.0", + "version": "~6.0.0", "type": "build" }, { "name": "jsii", - "version": "~5.7.0", + "version": "~6.0.0", "type": "build" }, { @@ -97,6 +97,7 @@ }, { "name": "ts-jest", + "version": "^29.4.12", "type": "build" }, { @@ -105,16 +106,17 @@ }, { "name": "typescript", + "version": "~6.0.0", "type": "build" }, { "name": "aws-cdk-lib", - "version": "^2.168.0", + "version": "^2.268.0", "type": "peer" }, { "name": "constructs", - "version": "^10.3.0", + "version": "^10.8.1", "type": "peer" } ], diff --git a/.projen/tasks.json b/.projen/tasks.json index 14df8ef..4f99653 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -308,7 +308,7 @@ }, "steps": [ { - "exec": "npx npm-check-updates@18 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-prettier,jest,jsii-diff,jsii-pacmak,prettier,projen,ts-jest,ts-node,typescript" + "exec": "npx npm-check-updates@18 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-prettier,jest,jsii-diff,jsii-pacmak,prettier,projen,ts-node" }, { "exec": "npm install" diff --git a/.projenrc.ts b/.projenrc.ts index 016c72f..d930fa8 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -2,14 +2,17 @@ import { ReleasableCommits, awscdk, github, javascript, release } from 'projen'; import { ArrowParens, NodePackageManager } from 'projen/lib/javascript'; import { SubPathExports } from './projenrc/sub-path-exports'; -let cdkVersion = '2.168.0'; +const cdkVersion = '2.268.0'; const project = new awscdk.AwsCdkConstructLibrary({ author: 'Open Construct Foundation', authorAddress: 'thorsten.hoeger@taimos.de', cdkVersion: cdkVersion, defaultReleaseBranch: 'main', - jsiiVersion: '~5.7.0', - constructsVersion: '10.3.0', + jsiiVersion: '~6.0.0', + typescriptVersion: '~6.0.0', + tsconfigDev: { compilerOptions: { types: ['node', 'jest'], rootDir: '.' } }, + jestOptions: { jestConfig: { maxWorkers: 1 } }, + constructsVersion: '10.8.1', name: '@open-constructs/aws-cdk', projenrcTs: true, repositoryUrl: 'https://github.com/open-constructs/aws-cdk-library.git', @@ -66,8 +69,8 @@ Closes #. distName: 'open-constructs-aws-cdk', module: 'open_constructs_aws_cdk', }, - workflowNodeVersion: '18.x', - minNodeVersion: '18.0.0', + workflowNodeVersion: '24.x', + minNodeVersion: '22.0.0', // publishToMaven: { // mavenGroupId: 'org.open-constructs', // mavenArtifactId: 'aws-cdk', @@ -77,7 +80,7 @@ Closes #. // packageId: 'OpenConstructs.AwsCdk', // dotNetNamespace: 'OpenConstructs.AwsCdk', // }, - devDeps: [`@aws-cdk/integ-runner@${cdkVersion}-alpha.0`, `@aws-cdk/integ-tests-alpha@${cdkVersion}-alpha.0`], + devDeps: ['@aws-cdk/integ-runner@2.205.1', `@aws-cdk/integ-tests-alpha@${cdkVersion}-alpha.0`, 'ts-jest@^29.4.12'], eslintOptions: { dirs: ['src', 'test'], prettier: true, @@ -109,4 +112,13 @@ project.addTask('integ:update', { new SubPathExports(project); +// These releases support the jsii 6 / TypeScript 6 compiler family. +project.addDevDeps( + 'ts-jest@^29.4.12', + 'jsii-docgen@^10.12.6', + '@typescript-eslint/parser@^8.69.0', + '@typescript-eslint/eslint-plugin@^8.69.0', +); +project.npmignore?.addPatterns('/.plans/', '/cdk.context.json', '/cdk.out*/', '/.env*'); + project.synth(); diff --git a/API.md b/API.md index 67011c9..19b8d3e 100644 --- a/API.md +++ b/API.md @@ -58,6 +58,8 @@ new aws_cur.CostReport(scope: Construct, id: string, props: CostReportProps) | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | --- @@ -70,6 +72,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -171,7 +214,7 @@ Check whether the given construct is a Resource. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | costReportName | string | The name of the cost report. | | reportBucket | aws-cdk-lib.aws_s3.IBucket | The S3 bucket that stores the cost report. | @@ -196,16 +239,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -246,6 +290,395 @@ The S3 bucket that stores the cost report. --- +### DnsValidatedCertificateV2 + +- *Implements:* aws-cdk-lib.aws_certificatemanager.ICertificate + +A native DNS-validated ACM certificate in a specific region. + +The construct creates `AWS::CertificateManager::Certificate` directly. If +the requested certificate region differs from the containing stack region, +it creates or uses a certificate stack and returns the ARN through a weak +`Fn::GetStackOutput` reference. + +#### Initializers + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +new aws_certificatemanager.DnsValidatedCertificateV2(scope: Construct, id: string, props: DnsValidatedCertificateV2Props) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | @open-constructs/aws-cdk.aws_certificatemanager.DnsValidatedCertificateV2Props | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +##### `props`Required + +- *Type:* @open-constructs/aws-cdk.aws_certificatemanager.DnsValidatedCertificateV2Props + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | +| applyRemovalPolicy | Apply a removal policy to the native certificate resource. | +| metricDaysToExpiry | Return the ACM `DaysToExpiry` metric in the certificate region. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + +##### `applyRemovalPolicy` + +```typescript +public applyRemovalPolicy(policy: RemovalPolicy): void +``` + +Apply a removal policy to the native certificate resource. + +###### `policy`Required + +- *Type:* aws-cdk-lib.RemovalPolicy + +--- + +##### `metricDaysToExpiry` + +```typescript +public metricDaysToExpiry(props?: MetricOptions): Metric +``` + +Return the ACM `DaysToExpiry` metric in the certificate region. + +###### `props`Optional + +- *Type:* aws-cdk-lib.aws_cloudwatch.MetricOptions + +--- + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | +| isOwnedResource | Returns true if the construct was created by CDK, and false otherwise. | +| isResource | Check whether the given construct is a Resource. | +| fromCertificateAttributes | Import an existing certificate without adopting it or creating resources. | +| isDnsValidatedCertificateV2 | Return whether an object is a `DnsValidatedCertificateV2`. | + +--- + +##### `isConstruct` + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +aws_certificatemanager.DnsValidatedCertificateV2.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +Use this method instead of `instanceof` to properly detect `Construct` +instances, even when the construct library is symlinked. + +Explanation: in JavaScript, multiple copies of the `constructs` library on +disk are seen as independent, completely different libraries. As a +consequence, the class `Construct` in each copy of the `constructs` library +is seen as a different class, and an instance of one class will not test as +`instanceof` the other class. `npm install` will not create installations +like this, but users may manually symlink construct libraries together or +use a monorepo tool: in those cases, multiple copies of the `constructs` +library can be accidentally installed, and `instanceof` will behave +unpredictably. It is safest to avoid using `instanceof`, and using +this type-testing method instead. + +###### `x`Required + +- *Type:* any + +Any object. + +--- + +##### `isOwnedResource` + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +aws_certificatemanager.DnsValidatedCertificateV2.isOwnedResource(construct: IConstruct) +``` + +Returns true if the construct was created by CDK, and false otherwise. + +###### `construct`Required + +- *Type:* constructs.IConstruct + +--- + +##### `isResource` + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +aws_certificatemanager.DnsValidatedCertificateV2.isResource(construct: IConstruct) +``` + +Check whether the given construct is a Resource. + +###### `construct`Required + +- *Type:* constructs.IConstruct + +--- + +##### `fromCertificateAttributes` + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +aws_certificatemanager.DnsValidatedCertificateV2.fromCertificateAttributes(scope: Construct, id: string, attrs: DnsValidatedCertificateV2Attributes) +``` + +Import an existing certificate without adopting it or creating resources. + +###### `scope`Required + +- *Type:* constructs.Construct + +--- + +###### `id`Required + +- *Type:* string + +--- + +###### `attrs`Required + +- *Type:* @open-constructs/aws-cdk.aws_certificatemanager.DnsValidatedCertificateV2Attributes + +--- + +##### `isDnsValidatedCertificateV2` + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +aws_certificatemanager.DnsValidatedCertificateV2.isDnsValidatedCertificateV2(value: any) +``` + +Return whether an object is a `DnsValidatedCertificateV2`. + +###### `value`Required + +- *Type:* any + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | +| stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | +| certificateArn | string | The ARN of the certificate. | +| certificateRef | aws-cdk-lib.interfaces.aws_certificatemanager.CertificateReference | A public ACM resource reference. | +| certificateRegion | string | The region in which the certificate is created. | +| certificateResource | aws-cdk-lib.aws_certificatemanager.CfnCertificate | Native certificate in certificateStack, also exposed as node.defaultChild. Overriding this resource changes the owning stack, not the wrapper's stack. | +| certificateStack | aws-cdk-lib.Stack | The stack that owns the native ACM certificate. | +| tags | aws-cdk-lib.TagManager | Tag manager for the native ACM certificate. | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `env`Required + +```typescript +public readonly env: ResourceEnvironment; +``` + +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment + +The environment this resource belongs to. + +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. + +--- + +##### `stack`Required + +```typescript +public readonly stack: Stack; +``` + +- *Type:* aws-cdk-lib.Stack + +The stack in which this resource is defined. + +--- + +##### `certificateArn`Required + +```typescript +public readonly certificateArn: string; +``` + +- *Type:* string + +The ARN of the certificate. + +Nested owners support consumers only within their top-level stack tree. + +--- + +##### `certificateRef`Required + +```typescript +public readonly certificateRef: CertificateReference; +``` + +- *Type:* aws-cdk-lib.interfaces.aws_certificatemanager.CertificateReference + +A public ACM resource reference. + +--- + +##### `certificateRegion`Required + +```typescript +public readonly certificateRegion: string; +``` + +- *Type:* string + +The region in which the certificate is created. + +--- + +##### `certificateResource`Required + +```typescript +public readonly certificateResource: CfnCertificate; +``` + +- *Type:* aws-cdk-lib.aws_certificatemanager.CfnCertificate + +Native certificate in certificateStack, also exposed as node.defaultChild. Overriding this resource changes the owning stack, not the wrapper's stack. + +--- + +##### `certificateStack`Required + +```typescript +public readonly certificateStack: Stack; +``` + +- *Type:* aws-cdk-lib.Stack + +The stack that owns the native ACM certificate. + +--- + +##### `tags`Required + +```typescript +public readonly tags: TagManager; +``` + +- *Type:* aws-cdk-lib.TagManager + +Tag manager for the native ACM certificate. + +--- + + ### Domain - *Implements:* @open-constructs/aws-cdk.aws_codeartifact.IDomain @@ -291,6 +724,8 @@ new aws_codeartifact.Domain(scope: Construct, id: string, props: DomainProps) | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | | addToResourcePolicy | Adds a statement to the Codeartifact domain resource policy. | | grant | Grants permissions to the specified grantee on this CodeArtifact domain. | @@ -306,6 +741,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -539,7 +1015,7 @@ The attributes of the domain to import. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | domainArn | string | The ARN (Amazon Resource Name) of this CodeArtifact domain. | | domainName | string | The name of this CodeArtifact domain. | @@ -567,16 +1043,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -711,6 +1188,8 @@ new aws_elasticache.IamUser(scope: Construct, id: string, props?: IamUserProps) | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | | grant | Grant the given identity the specified actions. | | grantConnect | Permits an IAM principal to perform connect to the user. | @@ -725,6 +1204,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -901,7 +1421,7 @@ Imports an existing IAM-enabled user from userId. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userArn | string | The ARN of the user. | | userId | string | The ID of the user. | @@ -927,16 +1447,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -1053,6 +1574,8 @@ new aws_ec2.InstanceConnectEndpoint(scope: Construct, id: string, props: Instanc | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | --- @@ -1065,6 +1588,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -1195,7 +1759,7 @@ Import an existing endpoint to the stack from its attributes. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | connections | aws-cdk-lib.aws_ec2.Connections | The connection object associated with the EC2 Instance Connect Endpoint. | | instanceConnectEndpointId | string | The ID of the EC2 Instance Connect Endpoint. | @@ -1220,16 +1784,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -1328,6 +1893,8 @@ new aws_redshiftserverless.Namespace(scope: Construct, id: string, props: Namesp | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | | addIamRole | Adds a role to the namespace. | @@ -1341,6 +1908,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -1487,7 +2095,7 @@ Imports an existing Namespace from attributes. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | namespaceArn | string | The namespace Arn. | | namespaceId | string | The namespace id. | @@ -1513,16 +2121,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -1634,6 +2243,8 @@ new aws_elasticache.NoPasswordRequiredUser(scope: Construct, id: string, props?: | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | --- @@ -1646,6 +2257,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -1776,7 +2428,7 @@ Imports an existing no password required user from attributes. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userArn | string | The ARN of the user. | | userId | string | The ID of the user. | @@ -1802,16 +2454,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -1909,6 +2562,8 @@ new aws_fsx.OntapFileSystem(scope: Construct, id: string, props: OntapFileSystem | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | --- @@ -1921,6 +2576,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -2051,7 +2747,7 @@ Import an existing FSx for NetApp ONTAP file system from the given properties. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | connections | aws-cdk-lib.aws_ec2.Connections | The security groups/rules used to allow network connections to the file system. | | dnsName | string | The management endpoint DNS name assigned to this file system. | @@ -2078,16 +2774,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -2213,17 +2910,60 @@ new aws_elasticache.PasswordUser(scope: Construct, id: string, props: PasswordUs | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | --- -##### `toString` +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` ```typescript -public toString(): string +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void ``` -Returns a string representation of this construct. +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- ##### `applyRemovalPolicy` @@ -2355,7 +3095,7 @@ Imports an existing password authentication user from attributes. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | generatedSecret | aws-cdk-lib.aws_secretsmanager.ISecret | The secret containing the generated password. | | userArn | string | The ARN of the user. | @@ -2382,16 +3122,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -2503,6 +3244,8 @@ new aws_codeartifact.Repository(scope: Construct, id: string, props: RepositoryP | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | | addToResourcePolicy | Adds a statement to the CodeArtifact repository resource policy. | | grant | Grants permissions to the specified grantee on this CodeArtifact repository. | @@ -2519,6 +3262,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -2762,7 +3546,7 @@ The attributes of the repository to import. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | domain | @open-constructs/aws-cdk.aws_codeartifact.IDomain | The domain that contains this repository. | | repositoryArn | string | The ARN (Amazon Resource Name) of this CodeArtifact repository. | @@ -2790,16 +3574,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -2938,6 +3723,8 @@ new aws_elasticache.ServerlessCache(scope: Construct, id: string, props: Serverl | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | | grant | Grant the given identity the specified actions. | | grantConnect | Permits an IAM principal to perform connect to the serverless cache. | @@ -2955,6 +3742,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -3185,7 +4013,7 @@ Imports an existing ServerlessCache from attributes. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | connections | aws-cdk-lib.aws_ec2.Connections | The connection object associated with the ElastiCache Serverless Cache. | | endpointAddress | string | The DNS hostname of the cache node. | @@ -3213,16 +4041,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -3359,6 +4188,8 @@ new aws_elasticache.UserGroup(scope: Construct, id: string, props: UserGroupProp | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | | addUser | Adds a user to the user group. | @@ -3372,6 +4203,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -3518,7 +4390,7 @@ Imports an existing user group from attributes. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userGroupArn | string | The ARN of the user group. | | userGroupId | string | The ID of the user group. | @@ -3543,16 +4415,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -3656,6 +4529,8 @@ new aws_redshiftserverless.Workgroup(scope: Construct, id: string, props: Workgr | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | +| with | Applies one or more mixins to this construct. | +| applyCrossStackReferenceStrength | Override the cross-stack reference strength for this resource. | | applyRemovalPolicy | Apply the given removal policy to this resource. | --- @@ -3668,6 +4543,47 @@ public toString(): string Returns a string representation of this construct. +##### `with` + +```typescript +public with(mixins: ...IMixin[]): IConstruct +``` + +Applies one or more mixins to this construct. + +Mixins are applied in order. The list of constructs is captured at the +start of the call, so constructs added by a mixin will not be visited. +Use multiple `with()` calls if subsequent mixins should apply to added +constructs. + +###### `mixins`Required + +- *Type:* ...constructs.IMixin[] + +--- + +##### `applyCrossStackReferenceStrength` + +```typescript +public applyCrossStackReferenceStrength(strength: ReferenceStrength): void +``` + +Override the cross-stack reference strength for this resource. + +When set, any cross-stack reference to this resource will use the specified +mechanism instead of the global default determined by the +`@aws-cdk/core:defaultCrossStackReferences` context key. This is useful for +selectively weakening specific references to avoid the "deadly embrace" problem +without changing the app-wide default. + +###### `strength`Required + +- *Type:* aws-cdk-lib.ReferenceStrength + +The reference strength to use for this resource. + +--- + ##### `applyRemovalPolicy` ```typescript @@ -3798,7 +4714,7 @@ Import an existing workgroup to the stack from its attributes. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | connections | aws-cdk-lib.aws_ec2.Connections | The connection object associated with the Redshift Serverless Workgroup. | | endpointAddress | string | The workgroup endpoint address. | @@ -3827,16 +4743,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -4263,6 +5180,244 @@ The lower limit for data storage the cache is set to use. --- +### DnsValidatedCertificateV2Attributes + +Attributes of an existing ACM certificate to import. + +#### Initializer + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +const dnsValidatedCertificateV2Attributes: aws_certificatemanager.DnsValidatedCertificateV2Attributes = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| certificateArn | string | The ARN of an existing ACM certificate. | + +--- + +##### `certificateArn`Required + +```typescript +public readonly certificateArn: string; +``` + +- *Type:* string + +The ARN of an existing ACM certificate. + +--- + +### DnsValidatedCertificateV2Props + +Properties for a DNS-validated ACM certificate. + +#### Initializer + +```typescript +import { aws_certificatemanager } from '@open-constructs/aws-cdk' + +const dnsValidatedCertificateV2Props: aws_certificatemanager.DnsValidatedCertificateV2Props = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| domainName | string | Fully qualified domain name to request a certificate for. | +| allowExport | boolean | Whether the public certificate can be exported. | +| certificateName | string | Value for the certificate's `Name` tag. | +| certificateRegion | string | Concrete region in which to create the certificate. | +| certificateStack | aws-cdk-lib.Stack | Explicit stack in which to create the certificate. | +| hostedZone | aws-cdk-lib.aws_route53.IHostedZone | Route 53 hosted zone used to validate every certificate domain name. | +| hostedZonesByDomain | {[ key: string ]: aws-cdk-lib.aws_route53.IHostedZone} | Route 53 hosted zones used to validate individual certificate domain names. | +| keyAlgorithm | aws-cdk-lib.aws_certificatemanager.KeyAlgorithm | Public/private key algorithm for the certificate. | +| removalPolicy | aws-cdk-lib.RemovalPolicy | Removal policy for the ACM certificate. | +| subjectAlternativeNames | string[] | Alternative domain names on the certificate. | +| transparencyLoggingEnabled | boolean | Whether ACM certificate transparency logging is enabled. | + +--- + +##### `domainName`Required + +```typescript +public readonly domainName: string; +``` + +- *Type:* string + +Fully qualified domain name to request a certificate for. + +Wildcards such as `*.example.com` are supported. Scalar tokens must be +valid in the native owner; consumer-owned parameters can create cycles. + +--- + +##### `allowExport`Optional + +```typescript +public readonly allowExport: boolean; +``` + +- *Type:* boolean +- *Default:* false + +Whether the public certificate can be exported. + +Exportable public certificates incur issuance and renewal charges. + +--- + +##### `certificateName`Optional + +```typescript +public readonly certificateName: string; +``` + +- *Type:* string +- *Default:* the construct path, truncated to 255 characters + +Value for the certificate's `Name` tag. + +--- + +##### `certificateRegion`Optional + +```typescript +public readonly certificateRegion: string; +``` + +- *Type:* string +- *Default:* the containing stack's region + +Concrete region in which to create the certificate. + +Cannot be combined with `certificateStack`. Omit this property for an +environment-agnostic same-stack certificate. CloudFront requires us-east-1. + +--- + +##### `certificateStack`Optional + +```typescript +public readonly certificateStack: Stack; +``` + +- *Type:* aws-cdk-lib.Stack +- *Default:* create or reuse a generated certificate stack when needed + +Explicit stack in which to create the certificate. + +Use this when the certificate stack needs a custom synthesizer, stack name, +permissions boundary, termination protection, or explicit lifecycle +ownership. The stack must be in the same app/stage, account, and partition +as the containing stack. Its region determines the certificate region. + +Nested owners support consumers only within their top-level stack tree. +Use a top-level owner for sharing outside that tree. +Cannot be combined with `certificateRegion`. + +--- + +##### `hostedZone`Optional + +```typescript +public readonly hostedZone: IHostedZone; +``` + +- *Type:* aws-cdk-lib.aws_route53.IHostedZone +- *Default:* use hostedZonesByDomain for exact per-domain validation + +Route 53 hosted zone used to validate every certificate domain name. + +Specify exactly one of `hostedZone` and `hostedZonesByDomain`. +A separate owner requires a concrete zone ID or a native public hosted +zone created in that owner. Imported token scope does not prove ownership. +Public delegation and actual account ownership remain caller preconditions. + +--- + +##### `hostedZonesByDomain`Optional + +```typescript +public readonly hostedZonesByDomain: {[ key: string ]: IHostedZone}; +``` + +- *Type:* {[ key: string ]: aws-cdk-lib.aws_route53.IHostedZone} +- *Default:* use hostedZone for every domain + +Route 53 hosted zones used to validate individual certificate domain names. + +Keys are the primary domain name and every subject alternative name. +Matching is case-insensitive and ignores one trailing dot. Specify exactly +one of `hostedZone` and `hostedZonesByDomain`. Apex and wildcard names +require distinct keys. There is no suffix matching or implicit SAN creation. +Names and the SAN list must be concrete in this mode. + +--- + +##### `keyAlgorithm`Optional + +```typescript +public readonly keyAlgorithm: KeyAlgorithm; +``` + +- *Type:* aws-cdk-lib.aws_certificatemanager.KeyAlgorithm +- *Default:* KeyAlgorithm.RSA_2048 + +Public/private key algorithm for the certificate. + +--- + +##### `removalPolicy`Optional + +```typescript +public readonly removalPolicy: RemovalPolicy; +``` + +- *Type:* aws-cdk-lib.RemovalPolicy +- *Default:* RemovalPolicy.DESTROY + +Removal policy for the ACM certificate. + +--- + +##### `subjectAlternativeNames`Optional + +```typescript +public readonly subjectAlternativeNames: string[]; +``` + +- *Type:* string[] +- *Default:* no subject alternative names + +Alternative domain names on the certificate. + +Single-zone validation supports fixed arrays of scalar tokens and lists +whose length resolves during synthesis. Resolved names are normalized and +checked for duplicates and zone authority. Opaque deployment-time lists +are unsupported. Empty or absent resolved lists omit the native SAN +property. Exact multi-zone mapping requires concrete names. + +--- + +##### `transparencyLoggingEnabled`Optional + +```typescript +public readonly transparencyLoggingEnabled: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether ACM certificate transparency logging is enabled. + +--- + ### DomainAttributes Interface representing the attributes of a CodeArtifact domain. @@ -5212,7 +6367,7 @@ const ontapFileSystemProps: aws_fsx.OntapFileSystemProps = { ... } | storageCapacityGiB | number | The storage capacity of the file system being created. | | vpc | aws-cdk-lib.aws_ec2.IVpc | The VPC to launch the file system in. | | backupId | string | The ID of the backup. | -| kmsKey | aws-cdk-lib.aws_kms.IKey | The KMS key used for encryption to protect your data at rest. | +| kmsKey | aws-cdk-lib.interfaces.aws_kms.IKeyRef | The KMS key used for encryption to protect your data at rest. | | removalPolicy | aws-cdk-lib.RemovalPolicy | Policy to apply when the file system is removed from the stack. | | securityGroup | aws-cdk-lib.aws_ec2.ISecurityGroup | Security Group to assign to this file system. | | storageType | aws-cdk-lib.aws_fsx.StorageType | The storage type for the file system that you're creating. | @@ -5268,10 +6423,10 @@ Specifies the backup to use if you're creating a file system from an existing ba ##### `kmsKey`Optional ```typescript -public readonly kmsKey: IKey; +public readonly kmsKey: IKeyRef; ``` -- *Type:* aws-cdk-lib.aws_kms.IKey +- *Type:* aws-cdk-lib.interfaces.aws_kms.IKeyRef - *Default:* the aws/fsx default KMS key for the AWS account being deployed into. The KMS key used for encryption to protect your data at rest. @@ -7441,7 +8596,7 @@ The principal to grant contribute permissions to. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | domainArn | string | The ARN of the Domain. | | domainName | string | The name of the Domain. | @@ -7469,16 +8624,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -7610,7 +8766,7 @@ Grant connect permissions to this user. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userArn | string | The ARN of the user. | | userId | string | The ID of the user. | @@ -7636,16 +8792,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -7712,7 +8869,7 @@ An EC2 Instance Connect Endpoint. | --- | --- | --- | | connections | aws-cdk-lib.aws_ec2.Connections | The network connections associated with this resource. | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | instanceConnectEndpointId | string | The ID of the EC2 Instance Connect Endpoint. | @@ -7748,16 +8905,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -7799,7 +8957,7 @@ A Redshift Serverless Namespace. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | namespaceArn | string | The namespace ARN. | | namespaceId | string | The namespace id. | @@ -7825,16 +8983,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -7900,7 +9059,7 @@ Interface for no password required users. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userArn | string | The ARN of the user. | | userId | string | The ID of the user. | @@ -7926,16 +9085,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -8001,7 +9161,7 @@ Interface for password-authenticated users. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userArn | string | The ARN of the user. | | userId | string | The ID of the user. | @@ -8027,16 +9187,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -8184,7 +9345,7 @@ The principal to grant permissions to. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | domain | @open-constructs/aws-cdk.aws_codeartifact.IDomain | The domain that contains the repository. | | repositoryArn | string | The ARN of the repository. | @@ -8212,16 +9373,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -8374,7 +9536,7 @@ Create a CloudWatch metric. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | connections | aws-cdk-lib.aws_ec2.Connections | The network connections associated with this resource. | | endpointAddress | string | The DNS hostname of the cache node. | @@ -8402,16 +9564,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -8501,7 +9664,7 @@ Interface for a User. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userArn | string | The ARN of the user. | | userId | string | The ID of the user. | @@ -8527,16 +9690,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -8602,7 +9766,7 @@ Interface for a User Group. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | userGroupArn | string | The ARN of the user group. | | userGroupId | string | The ID of the user group. | @@ -8627,16 +9791,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- @@ -8690,7 +9855,7 @@ A Redshift Serverless Workgroup. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| env | aws-cdk-lib.ResourceEnvironment | The environment this resource belongs to. | +| env | aws-cdk-lib.interfaces.ResourceEnvironment | The environment this resource belongs to. | | stack | aws-cdk-lib.Stack | The stack in which this resource is defined. | | connections | aws-cdk-lib.aws_ec2.Connections | The network connections associated with this resource. | | endpointAddress | string | The workgroup endpoint address. | @@ -8719,16 +9884,17 @@ The tree node. public readonly env: ResourceEnvironment; ``` -- *Type:* aws-cdk-lib.ResourceEnvironment +- *Type:* aws-cdk-lib.interfaces.ResourceEnvironment The environment this resource belongs to. -For resources that are created and managed by the CDK -(generally, those created by creating new class instances like Role, Bucket, etc.), -this is always the same as the environment of the stack they belong to; -however, for imported resources -(those obtained from static methods like fromRoleArn, fromBucketName, etc.), -that might be different than the stack they were imported into. +For resources that are created and managed in a Stack (those created by +creating new class instances like `new Role()`, `new Bucket()`, etc.), this +is always the same as the environment of the stack they belong to. + +For referenced resources (those obtained from referencing methods like +`Role.fromRoleArn()`, `Bucket.fromBucketName()`, etc.), they might be +different than the stack they were imported into. --- diff --git a/README.md b/README.md index 6feed6e..22f73d8 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,13 @@ By leveraging the power of AWS CDK, our library offers developers a wide range o ## Getting Started -To get started with the Open Constructs Library, you'll need to have Node.js and the AWS CDK installed on your machine (Other languages will follow very soon). Follow these steps to set up your project. +To get started with the Open Constructs Library, you'll need Node.js and the AWS CDK. JavaScript/TypeScript and Python packages are provided. Follow these steps to set up your JavaScript project. ### Prerequisites -- Node.js (version 18.x or later) -- AWS CDK (version 2.120.0 or later) +- Node.js (version 22.0.0 or later) +- AWS CDK library (version 2.268.0 or later) +- Constructs (version 10.8.1 or later) ### Installation @@ -35,7 +36,7 @@ npm install @open-constructs/aws-cdk 2. Import the constructs you need in your CDK stack: ```typescript -import { SomeConstruct } from '@open-constructs/aws-cdk'; +import { DnsValidatedCertificateV2 } from '@open-constructs/aws-cdk/aws-certificatemanager'; ``` 3. Follow the library documentation to see how to use the constructs in your application. @@ -44,6 +45,8 @@ import { SomeConstruct } from '@open-constructs/aws-cdk'; For more detailed documentation, including API references and examples, please visit our [documentation site](./API.md). +For native DNS-validated certificates, including CloudFront certificates in another region, see the [ACM module guide](./src/aws-certificatemanager/README.md). + ## Contributing We welcome contributions from the community! If you're interested in contributing to the Open Constructs Library, please read our [Contributing Guide](./CONTRIBUTING.md) for more information on how to get started. diff --git a/package-lock.json b/package-lock.json index d7c737f..ae371bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,15 @@ "version": "0.0.0", "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/integ-runner": "2.168.0-alpha.0", - "@aws-cdk/integ-tests-alpha": "2.168.0-alpha.0", + "@aws-cdk/integ-runner": "2.205.1", + "@aws-cdk/integ-tests-alpha": "2.268.0-alpha.0", "@types/jest": "^29.5.14", - "@types/node": "^18", - "@typescript-eslint/eslint-plugin": "^8", - "@typescript-eslint/parser": "^8", - "aws-cdk-lib": "2.168.0", + "@types/node": "^22", + "@typescript-eslint/eslint-plugin": "^8.69.0", + "@typescript-eslint/parser": "^8.69.0", + "aws-cdk-lib": "2.268.0", "commit-and-tag-version": "^12", - "constructs": "10.3.0", + "constructs": "10.8.1", "eslint": "^9", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^3.10.1", @@ -25,23 +25,23 @@ "eslint-plugin-prettier": "^5.5.4", "jest": "^29.7.0", "jest-junit": "^16", - "jsii": "~5.7.0", + "jsii": "~6.0.0", "jsii-diff": "^1.121.0", - "jsii-docgen": "^10.5.0", + "jsii-docgen": "^10.12.6", "jsii-pacmak": "^1.121.0", - "jsii-rosetta": "~5.7.0", + "jsii-rosetta": "~6.0.0", "prettier": "^3.7.4", "projen": "^0.98.30", - "ts-jest": "^29.4.6", + "ts-jest": "^29.4.12", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "typescript": "~6.0.0" }, "engines": { - "node": ">= 18.0.0" + "node": ">= 22.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.168.0", - "constructs": "^10.3.0" + "aws-cdk-lib": "^2.268.0", + "constructs": "^10.8.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -65,41 +65,34 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.237", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.237.tgz", - "integrity": "sha512-OlXylbXI52lboFVJBFLae+WB99qWmI121x/wXQHEMj2RaVNVbWE+OAHcDk2Um1BitUQCaTf9ki57B0Fuqx0Rvw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@aws-cdk/asset-kubectl-v20": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.4.tgz", - "integrity": "sha512-Ps2MkmjYgMyflagqQ4dgTElc7Vwpqj8spw8dQVFiSeaaMPsuDSNsPax3/HjuDuwqsmLdaCZc6umlxYLpL0kYDA==", + "version": "2.2.292", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.292.tgz", + "integrity": "sha512-d4aMFsAFj19FtxVyw8IzlUKv5Zu4sIvgnEjI2IU6IWBJgVbJ4aFnadANqYa+6MwB1CQbGOg0jh8WE77M+Nb/9A==", "dev": true, "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", - "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz", + "integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==", "dev": true, "license": "Apache-2.0" }, "node_modules/@aws-cdk/aws-service-spec": { - "version": "0.1.33", - "resolved": "https://registry.npmjs.org/@aws-cdk/aws-service-spec/-/aws-service-spec-0.1.33.tgz", - "integrity": "sha512-Hj9DwC00pLFt2SdRpmOskyVRMHZT55jhG2SWfAPlp8I29ectVziFe1p2OB/281AiFwmQ3Kx3f66T0XJHnzS55Q==", + "version": "0.1.206", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-service-spec/-/aws-service-spec-0.1.206.tgz", + "integrity": "sha512-UZwzUR61NDUyR/aewgRxmyDo+oYwssuf6Gv2FLE/Lc1D/ajSPgpAKpysjSV1LLa6vCZ5OSl7TYbfhu2/YgNyEg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-cdk/service-spec-types": "^0.0.100", - "@cdklabs/tskb": "^0.0.3" + "@aws-cdk/service-spec-types": "^0.0.272", + "@cdklabs/tskb": "^0.0.4" } }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "38.0.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz", - "integrity": "sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w==", + "version": "54.22.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.22.0.tgz", + "integrity": "sha512-vsOuFw4+UC5+m2YZLhCecxt6wibIOmXCszA4uXaBcde2tZWx06vp1z5ByHnJeLS83A11XpAkW/TiIBGCL6jxuQ==", "bundleDependencies": [ "jsonschema", "semver" @@ -107,12 +100,15 @@ "dev": true, "license": "Apache-2.0", "dependencies": { - "jsonschema": "^1.4.1", - "semver": "^7.6.3" + "jsonschema": "^1.5.0", + "semver": "^7.8.5" + }, + "engines": { + "node": ">= 18.0.0" } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { - "version": "1.4.1", + "version": "1.5.0", "dev": true, "inBundle": true, "license": "MIT", @@ -121,7 +117,7 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.6.3", + "version": "7.8.5", "dev": true, "inBundle": true, "license": "ISC", @@ -133,47 +129,43 @@ } }, "node_modules/@aws-cdk/integ-runner": { - "version": "2.168.0-alpha.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/integ-runner/-/integ-runner-2.168.0-alpha.0.tgz", - "integrity": "sha512-QGPaIOjdAwm406NXe4hgiW4UMoGnTYAgwwPMooPW+rx/WgwmFKOzCTOpm+3A2hykbwTMSep9gS74iCs5WgxmdA==", + "version": "2.205.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/integ-runner/-/integ-runner-2.205.1.tgz", + "integrity": "sha512-HovU1NvrdiGLTBnyB0ZvgsMFfRQMdH7UPQgitBBhMjQmo80Hh8CZSRn+Myyr9otYUsyt9Igxj5nPIlo9ISZuyA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-cdk/aws-service-spec": "0.1.33", - "aws-cdk": "2.168.0" + "@aws-cdk/aws-service-spec": "0.1.206" }, "bin": { "integ-runner": "bin/integ-runner" }, "engines": { - "node": ">= 14.15.0" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "node": ">= 18.0.0" } }, "node_modules/@aws-cdk/integ-tests-alpha": { - "version": "2.168.0-alpha.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/integ-tests-alpha/-/integ-tests-alpha-2.168.0-alpha.0.tgz", - "integrity": "sha512-4r2q4X+iDKef1xpOASP93oC8H7Q8HqsJwYG+6XILcA1lYHrstrf1lZBZHcZWVO8oEXaZTdz8/dZhtDWgum8nGg==", + "version": "2.268.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/integ-tests-alpha/-/integ-tests-alpha-2.268.0-alpha.0.tgz", + "integrity": "sha512-+ztFgUZODWS6tBAar5H2e3nTMfFXTiTu3rdfKovSCsuPiWovABkOXyRbBodaJEkI+ybVYvotlnhvwQM8yybW4Q==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">= 14.15.0" + "node": ">= 20.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.168.0", - "constructs": "^10.0.0" + "aws-cdk-lib": "^2.268.0", + "constructs": "^10.5.0" } }, "node_modules/@aws-cdk/service-spec-types": { - "version": "0.0.100", - "resolved": "https://registry.npmjs.org/@aws-cdk/service-spec-types/-/service-spec-types-0.0.100.tgz", - "integrity": "sha512-z+jaWh3AIIGK3IHFXd7E3jc4ltkHoVQoB9O1MngIp1UluC0l0eZNwwSkWGGI5Ov+5BHhCwmtviOZQTu2BhU7Kw==", + "version": "0.0.272", + "resolved": "https://registry.npmjs.org/@aws-cdk/service-spec-types/-/service-spec-types-0.0.272.tgz", + "integrity": "sha512-iMgPauIuSbFDcbf7o1NBQalbS63h8dpFz+h/WtwaWOGNItE3WHRjmnlujRQVClpwAsvm2+UpzHO6zb+ry5skfA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@cdklabs/tskb": "^0.0.3" + "@cdklabs/tskb": "^0.0.4" } }, "node_modules/@babel/code-frame": { @@ -714,9 +706,9 @@ "license": "MIT" }, "node_modules/@cdklabs/tskb": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@cdklabs/tskb/-/tskb-0.0.3.tgz", - "integrity": "sha512-JR+MuD4awAXvutu7HArephXfZm09GPTaSAQUqNcJB5+ZENRm4kV+L6vJL6Tn1xHjCcHksO+HAqj3gYtm5K94vA==", + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@cdklabs/tskb/-/tskb-0.0.4.tgz", + "integrity": "sha512-NFx1X0l7p5DyHtLLEyNeh1hPN4UN9hTkZkzFs/Mp+kFk7dpdINGmGVpCfRDjJ2DrcNSNENUmT+w8g73TvmBbTw==", "dev": true, "license": "Apache-2.0" }, @@ -775,9 +767,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -794,9 +786,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -1046,14 +1038,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/console/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@jest/core": { "version": "29.7.0", "dev": true, @@ -1100,14 +1084,6 @@ } } }, - "node_modules/@jest/core/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@jest/environment": { "version": "29.7.0", "dev": true, @@ -1122,14 +1098,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/environment/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@jest/expect": { "version": "29.7.0", "dev": true, @@ -1169,14 +1137,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/fake-timers/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@jest/globals": { "version": "29.7.0", "dev": true, @@ -1233,14 +1193,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "dev": true, @@ -1334,14 +1286,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/types/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "dev": true, @@ -1386,14 +1330,14 @@ } }, "node_modules/@jsii/check-node": { - "version": "1.113.0", - "resolved": "https://registry.npmjs.org/@jsii/check-node/-/check-node-1.113.0.tgz", - "integrity": "sha512-6iPLiQiSVn8/D89ycIpj78cMfmxOIU/F9RUTVYwLqKPw4cxpR+BCC4N83WKyGkZxhOxULLa9f5q+rkWq/vAMpA==", + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/@jsii/check-node/-/check-node-1.140.0.tgz", + "integrity": "sha512-NTiY2Nd9ZtACPZjaU+k9+oiaD191Yw0FoiUR7bB+NoOtB5nMEhrLKn5O/PYmCIZGK0G1JaJs+PmLcyj68J6RKw==", "dev": true, "license": "Apache-2.0", "dependencies": { "chalk": "^4.1.2", - "semver": "^7.7.2" + "semver": "^7.8.5" }, "engines": { "node": ">= 14.17.0" @@ -1611,14 +1555,6 @@ "@types/node": "*" } }, - "node_modules/@types/graceful-fs/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "dev": true, @@ -1669,13 +1605,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/normalize-package-data": { @@ -1702,20 +1638,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", - "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/type-utils": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1725,23 +1661,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", - "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1751,20 +1687,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", - "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.0", - "@typescript-eslint/types": "^8.50.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1774,18 +1710,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1796,9 +1732,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", - "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", "dev": true, "license": "MIT", "engines": { @@ -1809,21 +1745,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", - "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1833,14 +1769,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", "dev": true, "license": "MIT", "engines": { @@ -1852,21 +1788,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1876,36 +1812,36 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1915,19 +1851,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1938,13 +1874,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2220,9 +2156,9 @@ ] }, "node_modules/@xmldom/xmldom": { - "version": "0.9.8", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", - "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz", + "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==", "dev": true, "license": "MIT", "engines": { @@ -2513,28 +2449,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-cdk": { - "version": "2.168.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.168.0.tgz", - "integrity": "sha512-jYsHcXBknuocRsdDUMZbcDN4E9EQE2F8oxNb7DtD38bn4/7TJehexxYg9yeAOH1pA120bGwLDX8STcAmf0kGIg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "cdk": "bin/cdk" - }, - "engines": { - "node": ">= 14.15.0" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, "node_modules/aws-cdk-lib": { - "version": "2.168.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.168.0.tgz", - "integrity": "sha512-eZkXYJaCLKRY1XzFKWHPr/7vSMMRmhz8p2oZacfxzr6XrzWcD5xtoNx2qIf215jaAI5Hewg31AMGZvQRQVNajw==", + "version": "2.268.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.268.0.tgz", + "integrity": "sha512-bm0mWG0xxIJtV0vhkmnFy6TFgwqBowvXeRKudhAsjM0TMOEcyFPBbwjUNlAA6S1+AjUwEt5yaMrT0A/P97lUqg==", "bundleDependencies": [ + "@aws/cloudformation-validate", "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", "case", "fs-extra", "ignore", @@ -2542,105 +2464,85 @@ "minimatch", "punycode", "semver", - "table", "yaml", "mime-types" ], "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.208", - "@aws-cdk/asset-kubectl-v20": "^2.1.3", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^38.0.1", + "@aws-cdk/asset-awscli-v1": "2.2.292", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2", + "@aws-cdk/cloud-assembly-api": "^2.2.6", + "@aws-cdk/cloud-assembly-schema": "^54.11.0", + "@aws/cloudformation-validate": "1.8.0-beta", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.2.0", + "fs-extra": "^11.3.6", "ignore": "^5.3.2", - "jsonschema": "^1.4.1", + "jsonschema": "^1.5.0", "mime-types": "^2.1.35", - "minimatch": "^3.1.2", + "minimatch": "^10.2.5", "punycode": "^2.3.1", - "semver": "^7.6.3", - "table": "^6.8.2", - "yaml": "1.10.2" + "semver": "^7.8.5", + "yaml": "1.10.3" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 20.0.0" }, "peerDependencies": { - "constructs": "^10.0.0" + "constructs": "^10.5.0" } }, - "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { - "version": "1.0.2", + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.6", "dev": true, "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.17.1", - "dev": true, - "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "jsonschema": "^1.5.0", + "semver": "^7.8.4" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=54.5.0" } }, - "node_modules/aws-cdk-lib/node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/aws-cdk-lib/node_modules/@aws/cloudformation-validate": { + "version": "1.8.0-beta", "dev": true, "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": ">=20.0.0" } }, - "node_modules/aws-cdk-lib/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", "dev": true, "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "Apache-2.0" }, - "node_modules/aws-cdk-lib/node_modules/astral-regex": { - "version": "2.0.0", + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", "dev": true, "inBundle": true, "license": "MIT", "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, - "node_modules/aws-cdk-lib/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "5.0.9", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/aws-cdk-lib/node_modules/case": { @@ -2652,50 +2554,8 @@ "node": ">= 0.8.0" } }, - "node_modules/aws-cdk-lib/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.0.3", - "dev": true, - "inBundle": true, - "license": "BSD-3-Clause" - }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.2.0", + "version": "11.3.6", "dev": true, "inBundle": true, "license": "MIT", @@ -2723,23 +2583,8 @@ "node": ">= 4" } }, - "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.1.0", + "version": "6.2.1", "dev": true, "inBundle": true, "license": "MIT", @@ -2751,7 +2596,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.4.1", + "version": "1.5.0", "dev": true, "inBundle": true, "license": "MIT", @@ -2759,12 +2604,6 @@ "node": "*" } }, - "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { - "version": "4.4.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, "node_modules/aws-cdk-lib/node_modules/mime-db": { "version": "1.52.0", "dev": true, @@ -2787,15 +2626,18 @@ } }, "node_modules/aws-cdk-lib/node_modules/minimatch": { - "version": "3.1.2", + "version": "10.2.5", "dev": true, "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/aws-cdk-lib/node_modules/punycode": { @@ -2807,17 +2649,8 @@ "node": ">=6" } }, - "node_modules/aws-cdk-lib/node_modules/require-from-string": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.6.3", + "version": "7.8.5", "dev": true, "inBundle": true, "license": "ISC", @@ -2828,65 +2661,6 @@ "node": ">=10" } }, - "node_modules/aws-cdk-lib/node_modules/slice-ansi": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.8.2", - "dev": true, - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/aws-cdk-lib/node_modules/universalify": { "version": "2.0.1", "dev": true, @@ -2897,7 +2671,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/yaml": { - "version": "1.10.2", + "version": "1.10.3", "dev": true, "inBundle": true, "license": "ISC", @@ -3020,13 +2794,26 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -3834,13 +3621,11 @@ } }, "node_modules/constructs": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", - "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.8.1.tgz", + "integrity": "sha512-98yGXYyhePqPYh3cYu8nzBERmAhC0DONe3UD03okK0nehZ7hYP4wgZuf02a04+uOWxnTJ5Rpp5m0GRNpwyLGGA==", "dev": true, - "engines": { - "node": ">= 16.14.0" - } + "license": "Apache-2.0" }, "node_modules/conventional-changelog-config-spec": { "version": "2.1.0", @@ -5103,21 +4888,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "dev": true, @@ -5437,7 +5207,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6270,14 +6042,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-cli": { "version": "29.7.0", "dev": true, @@ -6392,16 +6156,6 @@ } } }, - "node_modules/jest-config/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "dev": true, @@ -6458,14 +6212,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-environment-node/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "dev": true, @@ -6498,14 +6244,6 @@ "fsevents": "^2.3.2" } }, - "node_modules/jest-haste-map/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-haste-map/node_modules/fsevents": { "version": "2.3.3", "dev": true, @@ -6592,14 +6330,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-mock/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "dev": true, @@ -6686,14 +6416,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-runtime": { "version": "29.7.0", "dev": true, @@ -6726,14 +6448,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runtime/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-runtime/node_modules/strip-bom": { "version": "4.0.0", "dev": true, @@ -6788,14 +6502,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-validate": { "version": "29.7.0", "dev": true, @@ -6841,14 +6547,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-worker": { "version": "29.7.0", "dev": true, @@ -6863,14 +6561,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-worker/node_modules/@types/node": { - "version": "20.11.30", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -6914,30 +6604,30 @@ } }, "node_modules/jsii": { - "version": "5.7.22", - "resolved": "https://registry.npmjs.org/jsii/-/jsii-5.7.22.tgz", - "integrity": "sha512-hphnBdNJMhiNOreQX/rdY2hifdKEY2SZ/sQSIxNkAWftj+hhgv34Y1wNijZAKfzUbHykGJCkD6ksSUbeCy6RyA==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jsii/-/jsii-6.0.12.tgz", + "integrity": "sha512-QGi87rQ853gJhjc2+M/UHHRAhIPxShGmhtZPGgVA1vmmPEOn0kKiP+3pWziDmPNZojXtgpCCdfF8neOMWUA+yA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsii/check-node": "1.113.0", - "@jsii/spec": "^1.113.0", + "@jsii/check-node": "1.140.0", + "@jsii/spec": "1.140.0", "case": "^1.6.3", "chalk": "^4", "fast-deep-equal": "^3.1.3", "log4js": "^6.9.1", - "semver": "^7.7.2", + "semver": "^7.8.5", "semver-intersect": "^1.5.0", "sort-json": "^2.0.1", - "spdx-license-list": "^6.10.0", - "typescript": "~5.7", - "yargs": "^17.7.2" + "spdx-license-list": "^6.12.0", + "typescript": "~6.0", + "yargs": "^17.7.3" }, "bin": { "jsii": "bin/jsii" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.16.0" } }, "node_modules/jsii-diff": { @@ -7020,26 +6710,55 @@ } }, "node_modules/jsii-docgen": { - "version": "10.11.7", - "resolved": "https://registry.npmjs.org/jsii-docgen/-/jsii-docgen-10.11.7.tgz", - "integrity": "sha512-B92spJU5Pv4AAcAeIJPa0D5JtQLcJan/O7IlbsWGcfSRe9I4HlyzjAkFVjN37dbO7eYCfSBPxwfZdgGZLNzdRQ==", + "version": "10.12.6", + "resolved": "https://registry.npmjs.org/jsii-docgen/-/jsii-docgen-10.12.6.tgz", + "integrity": "sha512-MShAZLUkLLO2B8UG4rm7nlLRaBY7ePdZVagl1I4d0r9qnResexL7cqJN4qEmxB7qsRZhpCY2tLhb3wlpZlH3cg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsii/spec": "^1.121.0", + "@jsii/spec": "^1.140.0", "case": "^1.6.3", "fast-glob": "^3.3.3", "fs-extra": "^10.1.0", - "jsii-reflect": "^1.121.0", - "json-stream-stringify": "^3.1.6", - "semver": "^7.7.3", - "yargs": "^16.2.0" + "jsii-reflect": "^1.140.0", + "json-stream-stringify": "^3.1.7", + "semver": "^7.8.5", + "yargs": "^16.2.2" }, "bin": { "jsii-docgen": "bin/jsii-docgen" }, "peerDependencies": { - "jsii-rosetta": "^1.85.0 || ~5.0.14 || ~5.1.2 || ~5.2.0 || ~5.3.0 || ~5.4.0 || ~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ~5.9.1" + "jsii-rosetta": "^1.85.0 || ~5.0.14 || ~5.1.2 || ~5.2.0 || ~5.3.0 || ~5.4.0 || ~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ~5.9.1 || ~6.0.0" + } + }, + "node_modules/jsii-docgen/node_modules/@jsii/spec": { + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/@jsii/spec/-/spec-1.140.0.tgz", + "integrity": "sha512-1rGS+iJtejQ3ybSJfV0if9PzZuSQmORbVF3MkqE0csatF//Rj8raCqptZLMX6rkdfZ8C2Jsw5hU4PrmuCzBk6A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 14.17.0" + } + }, + "node_modules/jsii-docgen/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, "node_modules/jsii-pacmak": { @@ -7142,18 +6861,18 @@ } }, "node_modules/jsii-reflect": { - "version": "1.121.0", - "resolved": "https://registry.npmjs.org/jsii-reflect/-/jsii-reflect-1.121.0.tgz", - "integrity": "sha512-TqZdP8GwGq5+Qv96yuNMWapkd8oXEYVRKEqIGwR1UICcLOOXwF5CKzVG9tzNk37neWkIkE4U2PG/4I+bvIcFMQ==", + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/jsii-reflect/-/jsii-reflect-1.140.0.tgz", + "integrity": "sha512-8bRD6WSQfWrB6/31sz92bi21HWaMVzvxZUgaJt/IiT3RqGKc57dO+idekn8WNpTBneE9vVhj24UDExHH8YfJAQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsii/check-node": "1.121.0", - "@jsii/spec": "1.121.0", + "@jsii/check-node": "1.140.0", + "@jsii/spec": "1.140.0", "chalk": "^4", "fs-extra": "^10.1.0", - "oo-ascii-tree": "^1.121.0", - "yargs": "^17.7.2" + "oo-ascii-tree": "^1.140.0", + "yargs": "^17.7.3" }, "bin": { "jsii-query": "bin/jsii-query", @@ -7163,16 +6882,12 @@ "node": ">= 14.17.0" } }, - "node_modules/jsii-reflect/node_modules/@jsii/check-node": { - "version": "1.121.0", - "resolved": "https://registry.npmjs.org/@jsii/check-node/-/check-node-1.121.0.tgz", - "integrity": "sha512-0mPTsD9PDx/+Kvi6xNfOMzcxWW/nzo74rp96vN5qVg8pZThtzHqR14X0z4E/SqfYVs6Tv+Xiu4ctRRFmwe4xtQ==", + "node_modules/jsii-reflect/node_modules/@jsii/spec": { + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/@jsii/spec/-/spec-1.140.0.tgz", + "integrity": "sha512-1rGS+iJtejQ3ybSJfV0if9PzZuSQmORbVF3MkqE0csatF//Rj8raCqptZLMX6rkdfZ8C2Jsw5hU4PrmuCzBk6A==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "chalk": "^4.1.2", - "semver": "^7.7.2" - }, "engines": { "node": ">= 14.17.0" } @@ -7193,9 +6908,9 @@ } }, "node_modules/jsii-reflect/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { @@ -7222,35 +6937,47 @@ } }, "node_modules/jsii-rosetta": { - "version": "5.7.23", - "resolved": "https://registry.npmjs.org/jsii-rosetta/-/jsii-rosetta-5.7.23.tgz", - "integrity": "sha512-NRvHqYH8Pb9D4S+O0nQrEKwCBp7m9cC/EtmagDKjABX93d5lE4KHpLfRvac9lpYgQkn0GaLQu9yYmVgsqR38sA==", + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/jsii-rosetta/-/jsii-rosetta-6.0.13.tgz", + "integrity": "sha512-Ru3fbLNvCYIwsdrhIAjr/HFk0qfg15ssCqq1Ie6TL8V1Rq56fBbIzmDsH8CF60WJA2ZLXrUXrmbtKgzdQvHjNg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsii/check-node": "1.113.0", - "@jsii/spec": "^1.113.0", - "@xmldom/xmldom": "^0.9.8", + "@jsii/check-node": "^1.140.0", + "@jsii/spec": "^1.140.0", + "@xmldom/xmldom": "^0.9.12", "chalk": "^4", "commonmark": "^0.31.2", "fast-glob": "^3.3.3", - "jsii": "~5.7.0", - "semver": "^7.7.2", + "jsii": "~6.0.0", + "semver": "^7.8.5", "semver-intersect": "^1.5.0", "stream-json": "^1.9.1", - "typescript": "~5.7", + "typescript": "~6.0", "workerpool": "^6.5.1", - "yargs": "^17.7.2" + "yargs": "^17.7.3" }, "bin": { "jsii-rosetta": "bin/jsii-rosetta" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.16.0" + } + }, + "node_modules/jsii-rosetta/node_modules/@jsii/spec": { + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/@jsii/spec/-/spec-1.140.0.tgz", + "integrity": "sha512-1rGS+iJtejQ3ybSJfV0if9PzZuSQmORbVF3MkqE0csatF//Rj8raCqptZLMX6rkdfZ8C2Jsw5hU4PrmuCzBk6A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 14.17.0" } }, "node_modules/jsii-rosetta/node_modules/cliui": { "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -7262,22 +6989,10 @@ "node": ">=12" } }, - "node_modules/jsii-rosetta/node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/jsii-rosetta/node_modules/yargs": { - "version": "17.7.2", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { @@ -7295,14 +7010,28 @@ }, "node_modules/jsii-rosetta/node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/jsii/node_modules/@jsii/spec": { + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/@jsii/spec/-/spec-1.140.0.tgz", + "integrity": "sha512-1rGS+iJtejQ3ybSJfV0if9PzZuSQmORbVF3MkqE0csatF//Rj8raCqptZLMX6rkdfZ8C2Jsw5hU4PrmuCzBk6A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 14.17.0" + } + }, "node_modules/jsii/node_modules/cliui": { "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -7314,22 +7043,10 @@ "node": ">=12" } }, - "node_modules/jsii/node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/jsii/node_modules/yargs": { - "version": "17.7.2", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { @@ -7347,6 +7064,8 @@ }, "node_modules/jsii/node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -7383,9 +7102,9 @@ "license": "MIT" }, "node_modules/json-stream-stringify": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz", - "integrity": "sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.7.tgz", + "integrity": "sha512-F4MWetLtY42YMaAKw5cV4e47zMD5aOT+tjjQWjX18ACtdkQ5Y/vrcfbcQ107Rh+MXjOCIx4KhW0wPmOvG8iQ5w==", "dev": true, "license": "MIT", "engines": { @@ -8042,9 +7761,9 @@ } }, "node_modules/oo-ascii-tree": { - "version": "1.121.0", - "resolved": "https://registry.npmjs.org/oo-ascii-tree/-/oo-ascii-tree-1.121.0.tgz", - "integrity": "sha512-Dwzge50NT4bUxynVLtn/eFnl5Vv+8thNDVhw2MFZf6t5DmtIWKCDdQGUrIhN6PMEloDXVvPIW//oZtooSkp79g==", + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/oo-ascii-tree/-/oo-ascii-tree-1.140.0.tgz", + "integrity": "sha512-swJJN43RwwtZAtNQu03W8rqNtTUvq9T4eGU4KupRq7I9/R3exHobbF/OOm5sl6tGcumsQrNU97NBx7x+LEIPgA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9762,9 +9481,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -10025,9 +9744,9 @@ "license": "CC0-1.0" }, "node_modules/spdx-license-list": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/spdx-license-list/-/spdx-license-list-6.10.0.tgz", - "integrity": "sha512-wF3RhDFoqdu14d1Prv6c8aNU0FSRuSFJpNjWeygIZcNZEwPxp7I5/Hwo8j6lSkBKWAIkSQrKefrC5N0lvOP0Gw==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/spdx-license-list/-/spdx-license-list-6.12.0.tgz", + "integrity": "sha512-+nUYqm3aZMSHbjsthK+i/HHI2okTElCvqwUd4k8QcSk+FTGgjq+fsdFj2wZOaX6XmR2JdWhf/NeflNnvKOjcnQ==", "dev": true, "license": "CC0-1.0", "engines": { @@ -10480,9 +10199,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -10493,19 +10212,19 @@ } }, "node_modules/ts-jest": { - "version": "29.4.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", - "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.3", + "semver": "^7.8.5", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -10522,7 +10241,7 @@ "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" + "typescript": ">=4.3 <7" }, "peerDependenciesMeta": { "@babel/core": { @@ -10741,9 +10460,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10786,7 +10505,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index cff92fc..8638a6f 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,15 @@ "organization": false }, "devDependencies": { - "@aws-cdk/integ-runner": "2.168.0-alpha.0", - "@aws-cdk/integ-tests-alpha": "2.168.0-alpha.0", + "@aws-cdk/integ-runner": "2.205.1", + "@aws-cdk/integ-tests-alpha": "2.268.0-alpha.0", "@types/jest": "^29.5.14", - "@types/node": "^18", - "@typescript-eslint/eslint-plugin": "^8", - "@typescript-eslint/parser": "^8", - "aws-cdk-lib": "2.168.0", + "@types/node": "^22", + "@typescript-eslint/eslint-plugin": "^8.69.0", + "@typescript-eslint/parser": "^8.69.0", + "aws-cdk-lib": "2.268.0", "commit-and-tag-version": "^12", - "constructs": "10.3.0", + "constructs": "10.8.1", "eslint": "^9", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^3.10.1", @@ -53,26 +53,26 @@ "eslint-plugin-prettier": "^5.5.4", "jest": "^29.7.0", "jest-junit": "^16", - "jsii": "~5.7.0", + "jsii": "~6.0.0", "jsii-diff": "^1.121.0", - "jsii-docgen": "^10.5.0", + "jsii-docgen": "^10.12.6", "jsii-pacmak": "^1.121.0", - "jsii-rosetta": "~5.7.0", + "jsii-rosetta": "~6.0.0", "prettier": "^3.7.4", "projen": "^0.98.30", - "ts-jest": "^29.4.6", + "ts-jest": "^29.4.12", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "typescript": "~6.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.168.0", - "constructs": "^10.3.0" + "aws-cdk-lib": "^2.268.0", + "constructs": "^10.8.1" }, "keywords": [ "cdk" ], "engines": { - "node": ">= 18.0.0" + "node": ">= 22.0.0" }, "main": "lib/index.js", "license": "Apache-2.0", @@ -82,6 +82,7 @@ "version": "0.0.0", "jest": { "coverageProvider": "v8", + "maxWorkers": 1, "testMatch": [ "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", "/@(src|test)/**/__tests__/**/*.ts?(x)", @@ -142,6 +143,7 @@ }, "exports": { ".": "./lib/index.js", + "./aws-certificatemanager": "./lib/aws-certificatemanager/index.js", "./aws-codeartifact": "./lib/aws-codeartifact/index.js", "./aws-cur": "./lib/aws-cur/index.js", "./aws-ec2": "./lib/aws-ec2/index.js", diff --git a/src/aws-certificatemanager/README.md b/src/aws-certificatemanager/README.md new file mode 100644 index 0000000..4118592 --- /dev/null +++ b/src/aws-certificatemanager/README.md @@ -0,0 +1,207 @@ +# ACM certificates + +`DnsValidatedCertificateV2` creates a DNS-validated public ACM certificate in a chosen AWS region. It uses native CloudFormation resources and can place the certificate in a separate stack, including `us-east-1` for CloudFront applications deployed from another region. Import it from `@open-constructs/aws-cdk/aws-certificatemanager`; the module is maintained and released as part of the Open Constructs Library. + +## Requirements + +Use Node.js 22 or newer, `aws-cdk-lib` 2.268.0 or newer, and `constructs` 10.8.1 or newer. The library provides JavaScript/TypeScript and Python bindings. Install the JavaScript package with its peers: + +```bash +npm install @open-constructs/aws-cdk 'aws-cdk-lib@^2.268.0' 'constructs@^10.8.1' +``` + +DNS validation requires a publicly delegated Route 53 zone in the certificate account. With no placement inputs, the certificate stays in the containing stack, including an environment-agnostic stack. Explicit `certificateRegion` and separate-stack placement require concrete regions. Separate owners accept concrete hosted-zone IDs or native public zones created inside that owner. Use an existing zone imported by ID or attributes, or an explicit application lookup. The construct does not perform lookups or SDK calls during synthesis. + +## CloudFront from another region + +CloudFront requires its viewer certificate in [`us-east-1`](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html). An explicit owner stack makes certificate lifecycle management straightforward: + +```typescript +import { App, Stack } from 'aws-cdk-lib'; +import { DnsValidatedCertificateV2 } from '@open-constructs/aws-cdk/aws-certificatemanager'; +import { Distribution } from 'aws-cdk-lib/aws-cloudfront'; +import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'; +import { HostedZone } from 'aws-cdk-lib/aws-route53'; + +const app = new App(); +const account = process.env.CDK_DEFAULT_ACCOUNT; +const certificates = new Stack(app, 'Certificates', { + env: { account, region: 'us-east-1' }, +}); +const application = new Stack(app, 'Application', { + env: { account, region: 'eu-central-1' }, +}); + +const zone = HostedZone.fromHostedZoneAttributes(certificates, 'Zone', { + hostedZoneId: 'Z1234567890', + zoneName: 'example.com', +}); + +const certificate = new DnsValidatedCertificateV2(application, 'Certificate', { + domainName: 'www.example.com', + hostedZone: zone, + certificateStack: certificates, +}); + +new Distribution(application, 'Distribution', { + certificate, + domainNames: ['www.example.com'], + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, +}); + +app.synth(); +``` + +Replace the example zone ID, names, account environment, and origin with your application's values. Importing a zone does not establish real DNS delegation. CloudFront accepting a tokenized ARN at synthesis does not certify its region, issuance, or eventual deployability. For regional services, omit placement to use the containing stack, or set `certificateRegion` to the service's region. An explicit owner's region is authoritative, including regions outside `us-east-1`. + +```mermaid +flowchart LR + DNS["Public Route 53 zone in the certificate account"] + subgraph Owner["Certificate stack: us-east-1"] + ACM["Native ACM certificate"] + Output["Certificate ARN output"] + ACM --> Output + end + subgraph Consumer["Application stack: eu-central-1"] + Reference["Fn::GetStackOutput"] + Distribution["CloudFront distribution"] + Reference --> Distribution + end + DNS -. "DNS validation" .-> ACM + Output --> Reference +``` + +## Placement and ownership + +Omit both placement inputs to create the certificate in the containing stack. Set `certificateRegion` to request a concrete region: a matching region stays local; a different region creates one generated owner per containing-stack address and target region. Different containing stacks receive different generated owners. Separate construct instances always create separate certificates. + +Pass `certificateStack` for an explicit owner with its own stack name, synthesizer, tags, permissions boundary, termination protection, or lifecycle. Its region determines the certificate region. It cannot be combined with `certificateRegion`, even with equal values. Distinct owners must match the app/stage, account, and partition. Unmanaged collisions with generated owner IDs are rejected; pass the existing stack explicitly instead. A stable owner ID does not protect a certificate from replacement when its construct is renamed or moved. + +For automatic CloudFront placement, request its region explicitly: + +```typescript +import { App, Stack, Tags } from 'aws-cdk-lib'; +import { DnsValidatedCertificateV2 } from '@open-constructs/aws-cdk/aws-certificatemanager'; +import { Distribution } from 'aws-cdk-lib/aws-cloudfront'; +import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'; +import { HostedZone } from 'aws-cdk-lib/aws-route53'; + +const app = new App(); +const application = new Stack(app, 'Application', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'eu-central-1' }, +}); + +const zone = HostedZone.fromHostedZoneAttributes(application, 'Zone', { + hostedZoneId: 'Z1234567890', + zoneName: 'example.com', +}); + +const certificate = new DnsValidatedCertificateV2(application, 'ViewerCertificate', { + domainName: 'www.example.com', + hostedZone: zone, + certificateRegion: 'us-east-1', +}); + +Tags.of(certificate).add('Service', 'website'); +Tags.of(certificate).add('Name', 'operations-name', { priority: 200 }); + +certificate.certificateResource.addPropertyOverride('CertificateTransparencyLoggingPreference', 'DISABLED'); + +new Distribution(application, 'Distribution', { + certificate, + domainNames: ['www.example.com'], + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, +}); + +app.synth(); +``` + +Regional default and explicit owner inference use the same API: + +```typescript +import { App, Stack } from 'aws-cdk-lib'; +import { DnsValidatedCertificateV2 } from '@open-constructs/aws-cdk/aws-certificatemanager'; +import { HostedZone } from 'aws-cdk-lib/aws-route53'; + +const app = new App(); +const account = process.env.CDK_DEFAULT_ACCOUNT; +const application = new Stack(app, 'Application', { env: { account, region: 'eu-central-1' } }); +const owner = new Stack(app, 'RegionalOwner', { env: { account, region: 'eu-west-1' } }); + +const zone = HostedZone.fromHostedZoneId(application, 'Zone', 'Z1234567890'); + +new DnsValidatedCertificateV2(application, 'Local', { domainName: 'api.example.com', hostedZone: zone }); + +new DnsValidatedCertificateV2(application, 'Owned', { + domainName: 'service.example.com', + hostedZone: zone, + certificateStack: owner, +}); + +app.synth(); +``` + +`certificateRegion` and `env.region` describe the native certificate region. `Stack.of(certificate)` and `certificate.stack` describe the wrapper's actual construct-tree stack. `certificateStack` owns the native ACM resource. Nested stacks inherit their parent's environment and lifecycle: a nested consumer can reference a top-level regional owner, and a parent can consume a nested owner's output through CDK's native nested references. Nested owners support consumers only within the same top-level stack tree, including ancestors, siblings, and descendants. Sharing outside that tree fails at synthesis; use a top-level `certificateStack` for external sharing. This boundary follows the actual ARN consumer, regardless of where the wrapper was constructed. A separately configured regional owner must be a top-level stack. + +## DNS validation + +Specify exactly one of `hostedZone` and `hostedZonesByDomain`. A single zone validates the primary name and SANs. In multi-zone mode, provide an exact entry for every primary/SAN name, with no unused entries. Matching ignores case and one trailing dot; concrete names are emitted in lowercase without that dot. The wildcard prefix is preserved. Duplicate normalized domain names or map keys are rejected. Only the map's own entries count; there is no implicit apex-domain fallback. + +```typescript +import { App, Stack } from 'aws-cdk-lib'; +import { DnsValidatedCertificateV2 } from '@open-constructs/aws-cdk/aws-certificatemanager'; +import { HostedZone } from 'aws-cdk-lib/aws-route53'; + +const app = new App(); +const stack = new Stack(app, 'Certificates', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' }, +}); + +const primaryZone = HostedZone.fromHostedZoneAttributes(stack, 'PrimaryZone', { + hostedZoneId: 'Z1234567890', + zoneName: 'example.com', +}); +const alternateZone = HostedZone.fromHostedZoneAttributes(stack, 'AlternateZone', { + hostedZoneId: 'Z0987654321', + zoneName: 'example.net', +}); + +new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + subjectAlternativeNames: ['api.example.net'], + hostedZonesByDomain: { + 'www.example.com': primaryZone, + 'api.example.net': alternateZone, + }, +}); + +app.synth(); +``` + +In single-zone mode, deployment-time scalar expressions are preserved; names that become concrete during synthesis are normalized consistently, and a created same-stack zone can have a tokenized zone ID. Lazy SAN lists are supported when their length resolves during synthesis; duplicate and authority checks then run against the resolved names. Empty lists, including `omitEmpty: true` and producers returning `undefined`, omit the native SAN property and validate only the primary name. Lists whose length remains unknown until deployment cannot generate the required per-name DNS options and produce a focused synthesis error; use a fixed-length array of string tokens instead. Multi-zone mode requires concrete names and a concrete SAN list. A native public hosted zone created in `certificateStack` retains its local reference. Other unresolved zone IDs remain unsupported across owners; importing a token into the owner does not make its origin local. Tokenized domain names must also be valid in the native owner: use concrete app/context values or owner-defined parameters. Consumer-owned parameters can create a reverse dependency and cycle with the ARN reference; CDK reports that cycle during synthesis. The `/hostedzone/` ID prefix is removed when importing into the owner. + +Where the zone name is available, validation checks DNS-label authority for the primary name and SANs. Known private zones and known different accounts are rejected. An arbitrary imported `IHostedZone` cannot prove privacy or account ownership merely from its construct scope; public status, ownership, and actual delegation remain caller preconditions. Apex and wildcard validation can share a record, and public CDK avoids redundant native options for that pair. + +## Options, tags, metrics, and escape hatches + +Standard options include `subjectAlternativeNames`, `transparencyLoggingEnabled` (enabled by default), `allowExport` (disabled by default), `keyAlgorithm` (RSA 2048 by default), and `certificateName`. Public ACM requests support RSA 2048 and the supported ECDSA P-256/P-384 algorithms; select one compatible with the consuming service. Exportable certificates incur issuance and renewal charges. + +The default `Name` tag is the wrapper's construct path truncated to 255 characters. `certificateName` sets that tag; it is not a physical certificate name. Use standard `Tags.of(certificate)` for tags, with ordinary CDK priorities, exclusions, and removals. For a deterministic `Name` override, specify a higher priority such as 200 as shown above. Containing-scope, app, and explicit-owner tags reach the native ACM resource through its tag manager. This proxy does not make arbitrary aspects traverse into a sibling owner. + +The default removal policy is `RemovalPolicy.DESTROY`; set `removalPolicy` or call `applyRemovalPolicy()` to change it. `metricDaysToExpiry()` returns `AWS/CertificateManager`'s `DaysToExpiry` metric using the minimum statistic, a one-day period, the certificate ARN dimension, and the certificate region. Metric options can customize the period or label. + +`certificateResource` is the public readonly typed `CfnCertificate` in `certificateStack`; `node.defaultChild` points to the same resource. Its property overrides affect that owning template; the pointer does not move the resource into the wrapper's containing stack. Subclasses can override the protected `createCertificateResource(scope, id, props)` factory and access `certificateResource`. The default implementation composes the public ACM `Certificate` construct. Factory props contain eagerly normalized inputs with opaque SAN lists withheld; the constructor applies shared late-name resolution after the factory returns, so an ordinary subclass override retains DNS normalization and validation. + +`DnsValidatedCertificateV2.fromCertificateAttributes(scope, id, { certificateArn })` returns ACM's existing `ICertificate` for an existing ARN. It creates no certificate, stack, or validation records, does not validate or adopt the resource, and does not expose V2 placement properties. The imported interface retains public CDK's import behavior. `isDnsValidatedCertificateV2()` identifies OCF-managed V2 constructs across package copies and returns false for ordinary imported certificates. + +## Deploying, replacing, and removing certificates + +Deploy the owner before the consumer; CDK's synthesized dependency orders stacks that consume the ARN. The construct applies weak cross-stack reference strength to the native certificate producer. A certificate with a top-level owner can be shared by its owner and other stacks: owner-local consumers and outputs retain a direct native `Ref`, while remote consumers use the native output appropriate to their stack. This also applies when the certificate was constructed locally with no placement inputs. Nested owners retain native references within their top-level stack tree and reject consumers outside it. Supported sharing needs no `crossRegionReferences` flag, provider Lambda, custom resource, IAM role, or log group. The application independently chooses its global `@aws-cdk/core:defaultCrossStackReferences` policy; the construct does not change it. + +[`Fn::GetStackOutput`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/intrinsic-function-reference-getstackoutput.html) resolves during consumer create/update operations. Changing the owner alone does not refresh a deployed consumer. Weak references avoid a strong export lock but do not permit deleting an in-use certificate. For replacement, create a separately named second certificate, deploy it, switch and deploy the distribution, verify it no longer uses the old certificate, then remove the old certificate. Do not assume replacement of an in-use certificate succeeds in one deployment. + +For removal, first detach or replace consumers and deploy those changes. Remove the certificate from its explicitly declared owner and deploy that owner while the stack definition still exists. You can then delete the empty owner stack separately. For generated owners, record the deployed stack name before removing the last construct: disappearance from the cloud assembly does not delete a remote stack. `RemovalPolicy.RETAIN` intentionally leaves the certificate for separate management. + +[ACM validation CNAMEs](https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html) can be reused by other certificates and are not automatically removed by this construct. Retain shared records. Cross-account certificates/DNS writes, external DNS providers, private ACM certificates, and automatic CNAME cleanup are outside this module's scope. + +For contributor validation, see the [integration fixture runbook](../../test/aws-certificatemanager/README.md). Report problems in [Open Constructs Library issues](https://github.com/open-constructs/aws-cdk-library/issues). diff --git a/src/aws-certificatemanager/dns-validated-certificate-v2.ts b/src/aws-certificatemanager/dns-validated-certificate-v2.ts new file mode 100644 index 0000000..0c34850 --- /dev/null +++ b/src/aws-certificatemanager/dns-validated-certificate-v2.ts @@ -0,0 +1,743 @@ +/** + * Adapted for the Open Constructs Library from DNS Validated Certificate V2. + * Copyright 2026 Gary Sassano + * + * This product includes software developed as part of the AWS Cloud Development Kit (AWS CDK). + * AWS Cloud Development Kit (AWS CDK) + * Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ +import { + TagManager, + Duration, + Lazy, + IResolveContext, + ReferenceStrength, + RemovalPolicy, + Resource, + Stack, + Stage, + Token, +} from 'aws-cdk-lib'; +import { + CertificateProps, + CertificateReference, + ICertificate, + KeyAlgorithm, + Certificate, + CertificateValidation, + CfnCertificate, +} from 'aws-cdk-lib/aws-certificatemanager'; +import { Metric, MetricOptions, Stats } from 'aws-cdk-lib/aws-cloudwatch'; +import { IHostedZone, CfnHostedZone, HostedZone } from 'aws-cdk-lib/aws-route53'; +import { RegionInfo } from 'aws-cdk-lib/region-info'; +import type { Construct } from 'constructs'; + +const GENERATED_OWNER_SYMBOL = Symbol.for( + '@open-constructs/aws-cdk.aws-certificatemanager.DnsValidatedCertificateV2.generatedOwner', +); + +const CONSTRUCT_SYMBOL = Symbol.for('@open-constructs/aws-cdk.aws-certificatemanager.DnsValidatedCertificateV2'); + +/** + * Properties for a DNS-validated ACM certificate. + */ +export interface DnsValidatedCertificateV2Props { + /** + * Fully qualified domain name to request a certificate for. + * + * Wildcards such as `*.example.com` are supported. Scalar tokens must be + * valid in the native owner; consumer-owned parameters can create cycles. + */ + readonly domainName: string; + + /** + * Route 53 hosted zone used to validate every certificate domain name. + * + * Specify exactly one of `hostedZone` and `hostedZonesByDomain`. + * A separate owner requires a concrete zone ID or a native public hosted + * zone created in that owner. Imported token scope does not prove ownership. + * Public delegation and actual account ownership remain caller preconditions. + * + * @default - use hostedZonesByDomain for exact per-domain validation + */ + readonly hostedZone?: IHostedZone; + + /** + * Route 53 hosted zones used to validate individual certificate domain names. + * + * Keys are the primary domain name and every subject alternative name. + * Matching is case-insensitive and ignores one trailing dot. Specify exactly + * one of `hostedZone` and `hostedZonesByDomain`. Apex and wildcard names + * require distinct keys. There is no suffix matching or implicit SAN creation. + * Names and the SAN list must be concrete in this mode. + * + * @default - use hostedZone for every domain + */ + readonly hostedZonesByDomain?: Record; + + /** + * Alternative domain names on the certificate. + * + * Single-zone validation supports fixed arrays of scalar tokens and lists + * whose length resolves during synthesis. Resolved names are normalized and + * checked for duplicates and zone authority. Opaque deployment-time lists + * are unsupported. Empty or absent resolved lists omit the native SAN + * property. Exact multi-zone mapping requires concrete names. + * + * @default - no subject alternative names + */ + readonly subjectAlternativeNames?: string[]; + + /** + * Whether the public certificate can be exported. + * + * Exportable public certificates incur issuance and renewal charges. + * + * @default false + */ + readonly allowExport?: boolean; + + /** + * Whether ACM certificate transparency logging is enabled. + * + * @default true + */ + readonly transparencyLoggingEnabled?: boolean; + + /** + * Value for the certificate's `Name` tag. + * + * @default - the construct path, truncated to 255 characters + */ + readonly certificateName?: string; + + /** + * Public/private key algorithm for the certificate. + * + * @default KeyAlgorithm.RSA_2048 + */ + readonly keyAlgorithm?: KeyAlgorithm; + + /** + * Concrete region in which to create the certificate. + * + * Cannot be combined with `certificateStack`. Omit this property for an + * environment-agnostic same-stack certificate. CloudFront requires us-east-1. + * + * @default - the containing stack's region + */ + readonly certificateRegion?: string; + + /** + * Explicit stack in which to create the certificate. + * + * Use this when the certificate stack needs a custom synthesizer, stack name, + * permissions boundary, termination protection, or explicit lifecycle + * ownership. The stack must be in the same app/stage, account, and partition + * as the containing stack. Its region determines the certificate region. + * + * Nested owners support consumers only within their top-level stack tree. + * Use a top-level owner for sharing outside that tree. + * Cannot be combined with `certificateRegion`. + * + * @default - create or reuse a generated certificate stack when needed + */ + readonly certificateStack?: Stack; + + /** + * Removal policy for the ACM certificate. + * + * @default RemovalPolicy.DESTROY + */ + readonly removalPolicy?: RemovalPolicy; +} + +/** + * A native DNS-validated ACM certificate in a specific region. + * + * The construct creates `AWS::CertificateManager::Certificate` directly. If + * the requested certificate region differs from the containing stack region, + * it creates or uses a certificate stack and returns the ARN through a weak + * `Fn::GetStackOutput` reference. + */ +export class DnsValidatedCertificateV2 extends Resource implements ICertificate { + /** Return whether an object is a `DnsValidatedCertificateV2`. */ + public static isDnsValidatedCertificateV2(value: any): value is DnsValidatedCertificateV2 { + return value !== null && typeof value === 'object' && CONSTRUCT_SYMBOL in value; + } + + /** Import an existing certificate without adopting it or creating resources. */ + public static fromCertificateAttributes( + scope: Construct, + id: string, + attrs: DnsValidatedCertificateV2Attributes, + ): ICertificate { + return Certificate.fromCertificateArn(scope, id, attrs.certificateArn); + } + + /** The ARN of the certificate. Nested owners support consumers only within their top-level stack tree. */ + public readonly certificateArn: string; + + /** The region in which the certificate is created. */ + public readonly certificateRegion: string; + + /** The stack that owns the native ACM certificate. */ + public readonly certificateStack: Stack; + + /** Tag manager for the native ACM certificate. */ + public readonly tags: TagManager; + + /** + * Native certificate in certificateStack, also exposed as node.defaultChild. + * Overriding this resource changes the owning stack, not the wrapper's stack. + */ + public readonly certificateResource: CfnCertificate; + private readonly props: DnsValidatedCertificateV2Props; + private readonly validationAuthorities: ValidationAuthority[]; + + public constructor(scope: Construct, id: string, props: DnsValidatedCertificateV2Props) { + const placement = resolvePlacement(scope, id, props); + super(scope, id, { region: placement.region }); + this.props = { + ...props, + domainName: normalizeConcreteDnsName(props.domainName), + subjectAlternativeNames: + props.subjectAlternativeNames === undefined || Token.isUnresolved(props.subjectAlternativeNames) + ? props.subjectAlternativeNames + : props.subjectAlternativeNames.map(normalizeConcreteDnsName), + }; + try { + Object.defineProperty(this, CONSTRUCT_SYMBOL, { value: true }); + + this.certificateRegion = placement.region; + const domainNames = certificateDomainNames(this.props); + assertUniqueDomainNames(domainNames); + const containingStack = placement.containingStack; + const usesSeparateStack = placement.separate; + + if (usesSeparateStack) { + if (Token.isUnresolved(containingStack.region)) { + throw new Error( + 'certificates created in a separate stack require the containing stack to have a concrete region', + ); + } + this.validateCrossPartitionReference(containingStack); + } + + const validationConfiguration = resolveValidationConfiguration(this.props, certificateDomainNames(this.props)); + this.validationAuthorities = validationConfiguration.authorities; + this.validateHostedZones(containingStack.account, usesSeparateStack); + const authorityErrors = this.validateHostedZoneAuthority(); + if (authorityErrors.length > 0) { + throw new Error(authorityErrors.join('; ')); + } + this.certificateStack = this.selectCertificateStack(containingStack, props, usesSeparateStack); + const validation = this.validationForCertificate(validationConfiguration, usesSeparateStack); + + const certificateScope: Construct = usesSeparateStack ? this.certificateStack : this; + const certificateProps = certificateProperties(this, this.props, validation); + this.certificateResource = this.createCertificateResource( + certificateScope, + usesSeparateStack ? `Certificate${this.node.addr}` : 'Certificate', + Token.isUnresolved(certificateProps.subjectAlternativeNames) + ? { ...certificateProps, subjectAlternativeNames: undefined } + : certificateProps, + ); + if (validationConfiguration.kind === 'single') { + this.configureSingleZoneNames(validationConfiguration.hostedZone); + } + + this.node.defaultChild = this.certificateResource; + this.tags = this.certificateResource.tags; + + if (props.removalPolicy !== undefined) { + this.certificateResource.applyRemovalPolicy(props.removalPolicy); + } + + this.certificateResource.applyCrossStackReferenceStrength(ReferenceStrength.WEAK); + const nativeArn = this.certificateResource.ref; + const ownerRoot = topLevelStack(this.certificateStack); + this.certificateArn = this.certificateStack.nested + ? Lazy.uncachedString({ + produce: context => { + if (topLevelStack(Stack.of(context.scope)) !== ownerRoot) { + throw new Error( + this.node.path + + ': a certificate owned by a nested stack cannot be consumed outside its top-level stack tree; use a top-level certificateStack for cross-stack sharing', + ); + } + return nativeArn; + }, + }) + : nativeArn; + + this.node.addValidation({ validate: () => this.validateHostedZoneAuthority() }); + } catch (error) { + throw new Error(`${this.node.path}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** A public ACM resource reference. */ + public get certificateRef(): CertificateReference { + return { + certificateArn: this.certificateArn, + }; + } + + /** Apply a removal policy to the native certificate resource. */ + public applyRemovalPolicy(policy: RemovalPolicy): void { + this.certificateResource.applyRemovalPolicy(policy); + } + + /** + * Create the native resource through public ACM composition. + * + * Props contain eagerly normalized inputs. Opaque SAN lists are withheld; + * the constructor applies shared synthesis-time name resolution afterwards. + */ + protected createCertificateResource(scope: Construct, id: string, props: CertificateProps): CfnCertificate { + const certificate = new Certificate(scope, id, props); + const resource = certificate.node.defaultChild; + if (!CfnCertificate.isCfnCertificate(resource)) { + throw new Error(`${this.node.path}: expected an ACM certificate resource`); + } + return resource; + } + + private configureSingleZoneNames(hostedZone: IHostedZone): void { + // Use the current resolution context so CDK can discover references during + // preparation and retain their originating stacks. Stack.resolve() here + // would flatten parameter/resource references before dependency discovery. + const names = (context: IResolveContext): string[] => { + const resolvedSans: unknown = context.resolve(this.props.subjectAlternativeNames ?? []); + const sans: unknown = resolvedSans === undefined ? [] : resolvedSans; + if (!Array.isArray(sans)) { + throw new Error( + `${this.node.path}: subjectAlternativeNames must resolve to a fixed-length list at synthesis so DNS validation options can be created; use a concrete array of string tokens for deployment-time values`, + ); + } + const values = [context.resolve(this.props.domainName), ...sans].map(value => + typeof value === 'string' ? normalizeConcreteDnsName(value) : Token.asString(value), + ); + assertUniqueDomainNames(values); + const errors = this.validateHostedZoneAuthority(values.map(domainName => ({ domainName, hostedZone }))); + if (errors.length > 0) { + throw new Error(`${this.node.path}: ${errors.join('; ')}`); + } + return values; + }; + this.certificateResource.domainName = Lazy.uncachedString({ produce: context => names(context)[0] }); + if (this.props.subjectAlternativeNames !== undefined) { + this.certificateResource.subjectAlternativeNames = Lazy.uncachedList( + { produce: context => names(context).slice(1) }, + { omitEmpty: true }, + ); + } + this.certificateResource.domainValidationOptions = Lazy.uncachedAny({ + produce: context => + names(context) + .filter(name => Token.isUnresolved(name) || !name.startsWith('*.') || !names(context).includes(name.slice(2))) + .map(domainName => ({ domainName, hostedZoneId: hostedZone.hostedZoneId.replace(/^\/hostedzone\//, '') })), + }); + } + + /** Return the ACM `DaysToExpiry` metric in the certificate region. */ + public metricDaysToExpiry(props?: MetricOptions): Metric { + return new Metric({ + period: Duration.days(1), + ...props, + dimensionsMap: { CertificateArn: this.certificateArn }, + metricName: 'DaysToExpiry', + namespace: 'AWS/CertificateManager', + region: this.certificateRegion, + statistic: Stats.MINIMUM, + }); + } + + private selectCertificateStack( + containingStack: Stack, + props: DnsValidatedCertificateV2Props, + usesSeparateStack: boolean, + ): Stack { + if (props.certificateStack !== undefined) { + this.validateCertificateStack(props.certificateStack, props.certificateStack.node.id); + return props.certificateStack; + } + + if (!usesSeparateStack) { + return containingStack; + } + + const stage = Stage.of(this); + if (stage === undefined) { + throw new Error('cross-stack certificates must be defined inside a CDK App or Stage'); + } + + const stackId = `dns-validated-certificate-stack-${containingStack.node.addr}-${this.certificateRegion}`; + const existing = stage.node.tryFindChild(stackId); + if (existing !== undefined) { + if (!Stack.isStack(existing)) { + throw new Error(`a construct named ${JSON.stringify(stackId)} already exists in the stage and is not a Stack`); + } + if (!(GENERATED_OWNER_SYMBOL in existing)) { + throw new Error( + `a stack named ${JSON.stringify(stackId)} already exists and is not a generated certificate owner; pass it as certificateStack`, + ); + } + this.validateCertificateStack(existing, stackId); + return existing; + } + + const owner = new Stack(stage, stackId, { + env: { + account: containingStack.account, + region: this.certificateRegion, + }, + tags: containingStack.tags.tagValues(), + }); + Object.defineProperty(owner, GENERATED_OWNER_SYMBOL, { value: true }); + return owner; + } + + private validateCertificateStack(stack: Stack, stackId: string): void { + const containingStack = Stack.of(this); + if (stack === containingStack) { + return; + } + const containingStage = Stage.of(containingStack); + const certificateStage = Stage.of(stack); + if (containingStage === undefined || certificateStage !== containingStage) { + throw new Error( + `certificate stack ${JSON.stringify(stackId)} must be in the same CDK App or Stage as the containing stack`, + ); + } + + if (Token.isUnresolved(stack.region) || stack.region !== this.certificateRegion) { + throw new Error( + `certificate stack ${JSON.stringify(stackId)} must be in region ${JSON.stringify(this.certificateRegion)}, got ${JSON.stringify(stack.region)}`, + ); + } + + const oneAccountIsUnresolved = Token.isUnresolved(containingStack.account) !== Token.isUnresolved(stack.account); + if ( + oneAccountIsUnresolved || + (!Token.isUnresolved(containingStack.account) && containingStack.account !== stack.account) + ) { + throw new Error( + `certificate stack ${JSON.stringify(stackId)} must be in account ${JSON.stringify(containingStack.account)}, got ${JSON.stringify(stack.account)}`, + ); + } + } + + private validateCrossPartitionReference(containingStack: Stack): void { + const containingPartition = RegionInfo.get(containingStack.region).partition; + const certificatePartition = RegionInfo.get(this.certificateRegion).partition; + if ( + containingPartition !== undefined && + certificatePartition !== undefined && + containingPartition !== certificatePartition + ) { + throw new Error( + `cross-partition references are not supported; the containing stack is in partition ${JSON.stringify(containingPartition)} and the certificate region is in partition ${JSON.stringify(certificatePartition)}`, + ); + } + } + + private validationForCertificate( + configuration: ValidationConfiguration, + usesSeparateStack: boolean, + ): CertificateValidation { + if (configuration.kind === 'single') { + const hostedZone = usesSeparateStack + ? this.importHostedZone(configuration.hostedZone, 0) + : configuration.hostedZone; + return CertificateValidation.fromDns(hostedZone); + } + + const importedZones = new Map(); + const hostedZonesByDomain = Object.fromEntries( + Object.entries(configuration.hostedZonesByDomain).map(([domainName, hostedZone], index) => { + if (!usesSeparateStack) { + return [domainName, hostedZone]; + } + + const cached = importedZones.get(hostedZone); + if (cached !== undefined) { + return [domainName, cached]; + } + + const imported = this.importHostedZone(hostedZone, index); + importedZones.set(hostedZone, imported); + return [domainName, imported]; + }), + ); + return CertificateValidation.fromDnsMultiZone(hostedZonesByDomain); + } + + private isOwnerLocalHostedZone(hostedZone: IHostedZone, owner = this.props.certificateStack): boolean { + const resource = hostedZone.node.defaultChild; + return ( + owner !== undefined && + CfnHostedZone.isCfnHostedZone(resource) && + Stack.of(resource) === owner && + hostedZone.hostedZoneId === resource.ref + ); + } + + private importHostedZone(hostedZone: IHostedZone, index: number): IHostedZone { + if (this.isOwnerLocalHostedZone(hostedZone, this.certificateStack)) { + return hostedZone; + } + const hostedZoneId = hostedZone.hostedZoneId.replace(/^\/hostedzone\//, ''); + + return HostedZone.fromHostedZoneId(this.certificateStack, `ValidationZone${index}${this.node.addr}`, hostedZoneId); + } + + private validateHostedZones(certificateAccount: string, usesSeparateStack: boolean): void { + for (const { hostedZone } of this.validationAuthorities) { + const resource = hostedZone.node.defaultChild; + const vpcs = CfnHostedZone.isCfnHostedZone(resource) ? Stack.of(hostedZone).resolve(resource.vpcs) : undefined; + if (Array.isArray(vpcs) && vpcs.length > 0) { + throw new Error( + `hosted zone ${JSON.stringify(hostedZone.node.path)} is private; public ACM certificates require a public hosted zone`, + ); + } + if ( + usesSeparateStack && + Token.isUnresolved(hostedZone.hostedZoneId) && + !this.isOwnerLocalHostedZone(hostedZone) + ) { + throw new Error( + 'certificates created in a separate stack require concrete hosted zone IDs or a native hosted zone in certificateStack; use HostedZone.fromLookup(), HostedZone.fromHostedZoneId(), or HostedZone.fromHostedZoneAttributes()', + ); + } + if ( + !Token.isUnresolved(certificateAccount) && + !Token.isUnresolved(hostedZone.env.account) && + hostedZone.env.account !== certificateAccount + ) { + throw new Error( + `hosted zone ${JSON.stringify(hostedZone.node.path)} must be in certificate account ${JSON.stringify(certificateAccount)}, got ${JSON.stringify(hostedZone.env.account)}`, + ); + } + } + } + + private validateHostedZoneAuthority(authorities = this.validationAuthorities): string[] { + const errors: string[] = []; + for (const { domainName, hostedZone } of authorities) { + const zoneName = tryGetHostedZoneName(hostedZone); + if ( + zoneName !== undefined && + !Token.isUnresolved(zoneName) && + !Token.isUnresolved(domainName) && + !isDomainNameInZone(domainName, zoneName) + ) { + errors.push( + `DNS zone ${normalizeDnsName(zoneName)} is not authoritative for certificate domain name ${domainName}`, + ); + } + } + return errors; + } +} + +interface ValidationAuthority { + readonly domainName: string; + readonly hostedZone: IHostedZone; +} + +interface SingleZoneValidationConfiguration { + readonly kind: 'single'; + readonly hostedZone: IHostedZone; + readonly authorities: ValidationAuthority[]; +} + +interface MultiZoneValidationConfiguration { + readonly kind: 'multi'; + readonly hostedZonesByDomain: Record; + readonly authorities: ValidationAuthority[]; +} + +type ValidationConfiguration = SingleZoneValidationConfiguration | MultiZoneValidationConfiguration; + +function resolveValidationConfiguration( + props: DnsValidatedCertificateV2Props, + domainNames: string[], +): ValidationConfiguration { + if ((props.hostedZone === undefined) === (props.hostedZonesByDomain === undefined)) { + throw new Error('specify exactly one of hostedZone and hostedZonesByDomain'); + } + + if (props.hostedZone !== undefined) { + return { + kind: 'single', + hostedZone: props.hostedZone, + authorities: domainNames.map(domainName => ({ + domainName, + hostedZone: props.hostedZone as IHostedZone, + })), + }; + } + + if (props.subjectAlternativeNames !== undefined && Token.isUnresolved(props.subjectAlternativeNames)) { + throw new Error( + 'hostedZonesByDomain cannot be used with an unresolved subjectAlternativeNames list; use hostedZone or provide a concrete list', + ); + } + + const unresolvedDomainName = domainNames.find(domainName => Token.isUnresolved(domainName)); + if (unresolvedDomainName !== undefined) { + throw new Error( + 'hostedZonesByDomain requires concrete domain names; use hostedZone when a domain name is unresolved', + ); + } + + const configuredZones = props.hostedZonesByDomain as Record; + const normalizedZones = new Map(); + for (const [key, hostedZone] of Object.entries(configuredZones)) { + const normalized = normalizeDnsName(key); + const existing = normalizedZones.get(normalized); + if (existing !== undefined) { + throw new Error( + `hostedZonesByDomain contains duplicate domain mappings ${JSON.stringify(existing.key)} and ${JSON.stringify(key)}`, + ); + } + normalizedZones.set(normalized, { key, hostedZone }); + } + + const hostedZonesByDomain: Record = {}; + const authorities: ValidationAuthority[] = []; + for (const domainName of domainNames) { + const matched = normalizedZones.get(normalizeDnsName(domainName))?.hostedZone; + if (matched === undefined) { + throw new Error( + `hostedZonesByDomain must contain a mapping for certificate domain name ${JSON.stringify(domainName)}`, + ); + } + hostedZonesByDomain[domainName] = matched; + authorities.push({ domainName, hostedZone: matched }); + } + + const certificateDomainNameKeys = new Set(domainNames.map(normalizeDnsName)); + for (const [normalized, { key }] of normalizedZones) { + if (!certificateDomainNameKeys.has(normalized)) { + throw new Error( + `hostedZonesByDomain contains a mapping for ${JSON.stringify(key)}, which is not a certificate domain name`, + ); + } + } + + return { kind: 'multi', hostedZonesByDomain, authorities }; +} + +function certificateProperties( + scope: Construct, + props: DnsValidatedCertificateV2Props, + validation: CertificateValidation, +): CertificateProps { + return { + allowExport: props.allowExport, + certificateName: props.certificateName ?? scope.node.path.slice(0, 255), + domainName: props.domainName, + keyAlgorithm: props.keyAlgorithm, + subjectAlternativeNames: props.subjectAlternativeNames?.length === 0 ? undefined : props.subjectAlternativeNames, + transparencyLoggingEnabled: props.transparencyLoggingEnabled, + validation, + }; +} + +function certificateDomainNames(props: DnsValidatedCertificateV2Props): string[] { + const domainNames = [props.domainName]; + if (props.subjectAlternativeNames !== undefined && !Token.isUnresolved(props.subjectAlternativeNames)) { + domainNames.push(...props.subjectAlternativeNames); + } + return domainNames; +} + +function assertUniqueDomainNames(domainNames: string[]): void { + const seen = new Map(); + for (const domainName of domainNames) { + if (Token.isUnresolved(domainName)) { + continue; + } + const normalized = normalizeDnsName(domainName); + const existing = seen.get(normalized); + if (existing !== undefined) { + throw new Error( + `certificate domain names must be unique; ${JSON.stringify(existing)} and ${JSON.stringify(domainName)} refer to the same DNS name`, + ); + } + seen.set(normalized, domainName); + } +} + +function tryGetHostedZoneName(hostedZone: IHostedZone): string | undefined { + try { + return hostedZone.zoneName; + } catch { + // ID-only hosted zone imports intentionally do not expose zoneName. + return undefined; + } +} + +function isDomainNameInZone(domainName: string, zoneName: string): boolean { + const normalizedDomainName = normalizeDnsName(domainName); + const normalizedZoneName = normalizeDnsName(zoneName); + return normalizedDomainName === normalizedZoneName || normalizedDomainName.endsWith(`.${normalizedZoneName}`); +} + +function normalizeDnsName(name: string): string { + const lowerCaseName = name.toLowerCase(); + return lowerCaseName.endsWith('.') ? lowerCaseName.slice(0, -1) : lowerCaseName; +} + +function normalizeConcreteDnsName(value: string): string { + return Token.isUnresolved(value) ? value : normalizeDnsName(value); +} + +/** Attributes of an existing ACM certificate to import. */ +export interface DnsValidatedCertificateV2Attributes { + /** The ARN of an existing ACM certificate. */ + readonly certificateArn: string; +} + +/** Resolve ownership metadata without allocating infrastructure. */ +function resolvePlacement( + scope: Construct, + id: string, + props: DnsValidatedCertificateV2Props, +): { + containingStack: Stack; + region: string; + separate: boolean; +} { + const path = `${scope.node.path}/${id}`; + if (props.certificateStack !== undefined && props.certificateRegion !== undefined) { + throw new Error(`${path}: specify at most one of certificateStack and certificateRegion`); + } + if (props.certificateRegion !== undefined && Token.isUnresolved(props.certificateRegion)) { + throw new Error( + `${path}: certificateRegion must be concrete; omit it for same-stack deployment or provide a concrete region for regional placement`, + ); + } + const containingStack = Stack.of(scope); + const region = props.certificateStack?.region ?? props.certificateRegion ?? containingStack.region; + const separate = + props.certificateStack !== undefined + ? props.certificateStack !== containingStack + : props.certificateRegion !== undefined && props.certificateRegion !== containingStack.region; + if (separate && Token.isUnresolved(region)) { + throw new Error(`${path}: a separate certificateStack must have a concrete region`); + } + return { containingStack, region, separate }; +} + +/** Find the reference boundary shared by a top-level stack and its nested descendants. */ +function topLevelStack(stack: Stack): Stack { + let current = stack; + while (current.nestedStackParent !== undefined) { + current = current.nestedStackParent; + } + return current; +} diff --git a/src/aws-certificatemanager/index.ts b/src/aws-certificatemanager/index.ts new file mode 100644 index 0000000..93466f9 --- /dev/null +++ b/src/aws-certificatemanager/index.ts @@ -0,0 +1 @@ +export * from './dns-validated-certificate-v2'; diff --git a/src/aws-fsx/ontap-file-system.ts b/src/aws-fsx/ontap-file-system.ts index 41975a4..dcd7e33 100644 --- a/src/aws-fsx/ontap-file-system.ts +++ b/src/aws-fsx/ontap-file-system.ts @@ -428,7 +428,7 @@ export class OntapFileSystem extends aws_fsx.FileSystemBase { fileSystemType: OntapFileSystem.DEFAULT_FILE_SYSTEM_TYPE, subnetIds: props.vpcSubnets.map(subnet => subnet.subnetId), backupId: props.backupId, - kmsKeyId: props.kmsKey?.keyId, + kmsKeyId: props.kmsKey?.keyRef.keyId, ontapConfiguration: { automaticBackupRetentionDays: ontapConfiguration.automaticBackupRetention?.toDays() ?? 30, dailyAutomaticBackupStartTime: ontapConfiguration.dailyAutomaticBackupStartTime?.toTimestamp(), diff --git a/src/index.ts b/src/index.ts index c7e45b9..cfb9d40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". +export * as aws_certificatemanager from './aws-certificatemanager'; export * as aws_codeartifact from './aws-codeartifact'; export * as aws_cur from './aws-cur'; export * as aws_ec2 from './aws-ec2'; diff --git a/test/aws-certificatemanager/README.md b/test/aws-certificatemanager/README.md new file mode 100644 index 0000000..ddc9eb3 --- /dev/null +++ b/test/aws-certificatemanager/README.md @@ -0,0 +1,39 @@ +# Certificate integration fixture + +This integration test requests a public certificate in `us-east-1`, attaches it to CloudFront from `eu-central-1`, and asserts ACM issuance and the deployed viewer certificate. Its HTTP origin is a synthetic endpoint; it does not test origin content. The explicit `certificateStack` supplies its `us-east-1` region; do not also supply `certificateRegion`. The revised API defaults to the containing stack when both inputs are omitted. No deployment or snapshot is implied by successful local synthesis. + +Run only this test after obtaining permission to deploy and clean up in the selected account, and permission to use a publicly delegated Route 53 zone in that account. Supply `CDK_DEFAULT_ACCOUNT`, `OCF_CERTIFICATE_INTEG_ZONE_ID`, and `OCF_CERTIFICATE_INTEG_ZONE_NAME`. The alias is `ocf-certificate-integ.`; reserve it for this test and ensure no other distribution uses it. Use a fixture whose identifiers are approved for the public snapshot. Do not commit local environment files or credentials. + +## Offline synthesis + +These synthetic identifiers are for assembly inspection only. They cannot validate a real certificate. `CDK_OUTDIR` keeps the assembly outside the repository. + +```bash +CDK_DEFAULT_ACCOUNT=123456789012 OCF_CERTIFICATE_INTEG_ZONE_ID=Z1234567890 OCF_CERTIFICATE_INTEG_ZONE_NAME=example.com CDK_OUTDIR=/tmp/ocf-certificate-integ-synth ./node_modules/.bin/ts-node --project tsconfig.dev.json test/aws-certificatemanager/integ.dns-validated-certificate-v2.ts +npm run integ -- --directory test/aws-certificatemanager --language typescript --list +``` + +Inspect `CertificateIntegOwner.template.json`, `CertificateIntegConsumer.template.json`, `CertificateIntegAssertions.template.json`, and `manifest.json`. Owner and consumer must contain no certificate-provider Lambda, role, policy, log group, or custom resource. Integration assertions have their own Lambda/IAM infrastructure in the assertion stack. The consumer must reference the owner's actual output with `Fn::GetStackOutput`, and the assembly must order the owner before consumer and both before assertions. + +## Authorized deployment + +Before deployment, verify the selected identity with `aws sts get-caller-identity`. Use `aws route53 get-hosted-zone` to verify the fixture's public status and compare its name servers with actual DNS delegation. Confirm bootstrap and CloudFormation permissions in both regions, and inspect existing CloudFront aliases. Save the complete hosted-zone record list before the run; ACM records can already be shared by other certificates. + +Review the installed runner's help. For the first deployment of this new test, run: + +```bash +npm run integ:update -- --directory test/aws-certificatemanager --language typescript --parallel-regions us-east-1 --max-workers 1 --strict --disable-update-workflow integ.dns-validated-certificate-v2.ts +npm run integ -- --directory test/aws-certificatemanager --language typescript --strict integ.dns-validated-certificate-v2.ts +``` + +Keep the runner's default cleanup enabled. The runner region is only `us-east-1`; the app already declares both actual regions. `--disable-update-workflow` is justified only for the initial test with no prior snapshot; subsequent changes need an assessment of update replay. A missing snapshot before the first deployment is expected. Do not create or edit a snapshot to stand in for a deployment. + +Capture the certificate ARN and distribution ID from the consumer outputs while deployed. The owner's generated ARN output is a direct native resource reference. An owner output can also use `certificate.certificateArn` directly, including when the same certificate object is shared with remote consumers. Verify ACM `describe-certificate` reports `ISSUED`, CloudFront `get-distribution` reports `Deployed` with that viewer ARN, and `get-template` on the consumer preserves the native output reference. Save the runner assertions and exact versions/commands. If deployment fails, inspect CloudFormation events and finish scoped cleanup before retrying. + +## Independent cleanup and snapshot review + +After runner cleanup, independently verify deletion of `CertificateIntegOwner` in `us-east-1`, `CertificateIntegConsumer` in `eu-central-1`, and `CertificateIntegAssertions` in `us-east-1`. ACM must return `ResourceNotFoundException` for the recorded ARN; CloudFront must return `NoSuchDistribution` for the recorded ID. A clean snapshot comparison alone proves none of these deletions. + +Compare Route 53 records with the saved pre-run list. Preserve the zone and every pre-existing record. Remove a test-created validation CNAME only when it is confirmed unshared and cleanup authorization covers it; otherwise record its retained owner. ACM validation CNAMEs are not automatically deleted with the certificate. + +Review the runner-generated snapshot for account/region normalization, external fixture identifiers, local absolute paths, and unrelated assets. Do not replace deployed identifiers with invented placeholders and claim that modified snapshot was deployed. Include only the runner's verified snapshot and approved fixture data in the contribution. diff --git a/test/aws-certificatemanager/dns-validated-certificate-v2.test.ts b/test/aws-certificatemanager/dns-validated-certificate-v2.test.ts new file mode 100644 index 0000000..7a59575 --- /dev/null +++ b/test/aws-certificatemanager/dns-validated-certificate-v2.test.ts @@ -0,0 +1,1983 @@ +import { + App, + BootstraplessSynthesizer, + CfnOutput, + CfnParameter, + Duration, + Fn, + Lazy, + NestedStack, + RemovalPolicy, + Stack, + Stage, + Tags, + Token, + Validations, +} from 'aws-cdk-lib'; +import { Annotations, Match, Template } from 'aws-cdk-lib/assertions'; +import { CfnCertificate, KeyAlgorithm } from 'aws-cdk-lib/aws-certificatemanager'; +import { CloudFrontWebDistribution, Distribution, ViewerCertificate } from 'aws-cdk-lib/aws-cloudfront'; +import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'; +import { Alarm, Dashboard, GraphWidget } from 'aws-cdk-lib/aws-cloudwatch'; +import { Vpc } from 'aws-cdk-lib/aws-ec2'; +import { CfnHostedZone, HostedZone, PrivateHostedZone, PublicHostedZone } from 'aws-cdk-lib/aws-route53'; +import { Construct } from 'constructs'; +import { DnsValidatedCertificateV2 } from '../../src/aws-certificatemanager'; + +const ACCOUNT = '111111111111'; +const OTHER_ACCOUNT = '222222222222'; +const WEAK_REFERENCE_CONTEXT = { + '@aws-cdk/core:defaultCrossStackReferences': 'weak', +}; + +describe('certificate placement and references', () => { + test('creates a native certificate in an explicitly requested us-east-1 support stack', () => { + const { app, stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + subjectAlternativeNames: ['api.example.com'], + }); + new CfnOutput(stack, 'CertificateArn', { value: certificate.certificateArn }); + + const certificateStack = generatedCertificateStack(app, stack); + Template.fromStack(certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + DomainValidationOptions: Match.arrayWith([ + { DomainName: 'test.example.com', HostedZoneId: 'Z123456' }, + { DomainName: 'api.example.com', HostedZoneId: 'Z123456' }, + ]), + SubjectAlternativeNames: ['api.example.com'], + ValidationMethod: 'DNS', + }); + const certificateResources = Template.fromStack(certificateStack).toJSON().Resources as Record< + string, + { Type: string } + >; + expect(Object.values(certificateResources).map(resource => resource.Type)).toEqual([ + 'AWS::CertificateManager::Certificate', + ]); + Template.fromStack(stack).hasOutput('CertificateArn', { + Value: weakCertificateArnReference(stack), + }); + expect(stack.dependencies).toContain(certificateStack); + expect(certificate.certificateRegion).toBe('us-east-1'); + expect(certificate.certificateStack).toBe(certificateStack); + }); + + test('creates the certificate in the containing stack when regions match', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 1); + expect(certificate.certificateStack).toBe(stack); + expect(app.node.tryFindChild(`dns-validated-certificate-stack-${stack.node.addr}-us-east-1`)).toBeUndefined(); + }); + + test('creates a certificate in an explicit non-default region', () => { + const { app, stack, hostedZone } = crossRegionFixture(); + + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'test.example.com', + hostedZone, + certificateRegion: 'us-west-2', + }); + + expect(certificate.certificateRegion).toBe('us-west-2'); + expect(generatedCertificateStack(app, stack, 'us-west-2').region).toBe('us-west-2'); + }); + + test('reuses one generated stack for multiple certificates', () => { + const { app, stack, hostedZone } = crossRegionFixture(); + + const first = new DnsValidatedCertificateV2(stack, 'FirstCertificate', { + certificateRegion: 'us-east-1', + domainName: 'first.example.com', + hostedZone, + }); + const second = new DnsValidatedCertificateV2(stack, 'SecondCertificate', { + certificateRegion: 'us-east-1', + domainName: 'second.example.com', + hostedZone, + }); + + expect(first.certificateStack).toBe(second.certificateStack); + Template.fromStack(generatedCertificateStack(app, stack)).resourceCountIs( + 'AWS::CertificateManager::Certificate', + 2, + ); + }); + + test('an explicit owner separates certificates even in the same region', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'test.example.com', + hostedZone, + certificateStack: createStack(app, 'Certificates', 'us-east-1'), + }); + new CfnOutput(stack, 'CertificateArn', { value: certificate.certificateArn }); + + expect(certificate.certificateStack).toBe(app.node.findChild('Certificates')); + Template.fromStack(stack).hasOutput('CertificateArn', { + Value: weakCertificateArnReference(stack, 'Certificates'), + }); + }); + + test('uses an explicit certificate stack with its custom synthesizer', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const certificateStack = new Stack(app, 'Certificates', { + env: { account: ACCOUNT, region: 'us-east-1' }, + stackName: 'shared-certificates', + synthesizer: new BootstraplessSynthesizer(), + terminationProtection: true, + }); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateStack, + domainName: 'test.example.com', + hostedZone, + }); + new CfnOutput(stack, 'CertificateArn', { value: certificate.certificateArn }); + + expect(certificate.certificateStack).toBe(certificateStack); + expect(certificateStack.stackName).toBe('shared-certificates'); + expect(certificateStack.terminationProtection).toBe(true); + Template.fromStack(certificateStack).resourceCountIs('AWS::CertificateManager::Certificate', 1); + }); + + test('generated stack inherits the containing stack account and stack tags', () => { + const app = createApp(); + const stack = new Stack(app, 'Stack', { + env: { account: ACCOUNT, region: 'eu-west-1' }, + tags: { team: 'edge' }, + }); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + expect(certificate.certificateStack.account).toBe(ACCOUNT); + expect(certificate.certificateStack.tags.tagValues()).toEqual({ team: 'edge' }); + }); + + test('works with CloudFront using the current certificate reference contract', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + new Distribution(stack, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('example.com') }, + domainNames: ['test.example.com'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: { + ViewerCertificate: { + AcmCertificateArn: weakCertificateArnReference(stack), + SslSupportMethod: 'sni-only', + }, + }, + }); + }); + + test('exposes the public certificate ARN reference contract', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + expect(certificate.certificateRef).toEqual({ + certificateArn: certificate.certificateArn, + }); + }); + + test('does not emit the global default-reference warning when the app selects weak references', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + new CfnOutput(stack, 'CertificateArn', { value: certificate.certificateArn }); + + Template.fromStack(stack); + expect( + Annotations.fromStack(stack).findWarning( + '*', + Match.stringLikeRegexp('No cross-stack-reference strength configured'), + ), + ).toEqual([]); + }); +}); + +describe('DNS validation', () => { + test('supports one hosted zone per certificate domain name', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const exampleCom = HostedZone.fromHostedZoneAttributes(stack, 'ExampleCom', { + hostedZoneId: 'ZEXAMPLECOM', + zoneName: 'example.com', + }); + const exampleNet = HostedZone.fromHostedZoneAttributes(stack, 'ExampleNet', { + hostedZoneId: 'ZEXAMPLENET', + zoneName: 'example.net', + }); + + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZonesByDomain: { + 'www.example.com': exampleCom, + 'api.example.net': exampleNet, + }, + subjectAlternativeNames: ['api.example.net'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainValidationOptions: [ + { DomainName: 'www.example.com', HostedZoneId: 'ZEXAMPLECOM' }, + { DomainName: 'api.example.net', HostedZoneId: 'ZEXAMPLENET' }, + ], + }); + }); + + test('matches multi-zone keys case-insensitively and without a trailing dot', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'WWW.Example.Com', + hostedZonesByDomain: { 'www.example.com.': hostedZone }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainName: 'www.example.com', + DomainValidationOptions: [{ DomainName: 'www.example.com', HostedZoneId: 'Z123456' }], + }); + }); + + test('strips the Route 53 hosted zone resource prefix in a support stack', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', '/hostedzone/Z123456'); + + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainValidationOptions: Match.arrayWith([{ DomainName: 'test.example.com', HostedZoneId: 'Z123456' }]), + }); + }); + + test('allows an unresolved hosted zone ID when the certificate stays in the containing stack', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = new PublicHostedZone(stack, 'HostedZone', { + zoneName: 'example.com', + }); + + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + Template.fromStack(stack).resourceCountIs('AWS::Route53::HostedZone', 1); + Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 1); + }); + + test('rejects an unresolved hosted zone ID for a separate certificate stack', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const hostedZone = new PublicHostedZone(stack, 'HostedZone', { + zoneName: 'example.com', + }); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/require concrete hosted zone IDs/); + }); + + test('rejects a primary domain outside the hosted zone', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'notexample.com', + hostedZone, + }), + ).toThrow(/DNS zone example\.com is not authoritative for certificate domain name notexample\.com/); + }); + + test('rejects a subject alternative name outside the single hosted zone', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone, + subjectAlternativeNames: ['api.example.net'], + }), + ).toThrow(/DNS zone example\.com is not authoritative for certificate domain name api\.example\.net/); + }); + + test('rejects a mismatched zone in multi-zone validation', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const wrongZone = HostedZone.fromHostedZoneAttributes(stack, 'WrongZone', { + hostedZoneId: 'ZWRONG', + zoneName: 'example.net', + }); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZonesByDomain: { 'www.example.com': wrongZone }, + }), + ).toThrow(/DNS zone example\.net is not authoritative for certificate domain name www\.example\.com/); + }); + + test('rejects a hosted zone from a different concrete account', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const hostedZoneStack = new Stack(app, 'HostedZoneStack', { + env: { account: OTHER_ACCOUNT, region: 'us-east-1' }, + }); + const hostedZone = HostedZone.fromHostedZoneId(hostedZoneStack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/hosted zone .* must be in certificate account/); + }); + + test('accepts case differences, trailing dots, and wildcard names', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'Z123456', + zoneName: 'Example.COM.', + }); + + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: '*.example.com', + hostedZone, + subjectAlternativeNames: ['API.Example.Com.'], + }); + + expect(() => Template.fromStack(stack)).not.toThrow(); + }); + + test('skips authority validation for an ID-only hosted zone import', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'unknown.example.net', + hostedZone, + }); + + expect(() => Template.fromStack(stack)).not.toThrow(); + }); + + test('requires exactly one hosted zone configuration', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Missing', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + }), + ).toThrow(/exactly one of hostedZone and hostedZonesByDomain/); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Both', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + hostedZonesByDomain: { 'test.example.com': hostedZone }, + }), + ).toThrow(/exactly one of hostedZone and hostedZonesByDomain/); + }); + + test('requires a multi-zone mapping for every certificate domain name', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZonesByDomain: { 'www.example.com': hostedZone }, + subjectAlternativeNames: ['api.example.com'], + }), + ).toThrow(/mapping for certificate domain name "api\.example\.com"/); + }); + + test('rejects duplicate normalized multi-zone keys', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZonesByDomain: { + 'WWW.EXAMPLE.COM': hostedZone, + 'www.example.com.': hostedZone, + }, + }), + ).toThrow(/duplicate domain mappings/); + }); + + test('rejects an unresolved SAN list with multi-zone validation', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + const subjectAlternativeNames = Lazy.list({ + produce: () => ['api.example.com'], + }); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZonesByDomain: { 'www.example.com': hostedZone }, + subjectAlternativeNames, + }), + ).toThrow(/cannot be used with an unresolved subjectAlternativeNames list/); + }); + + test('rejects an unresolved domain name with multi-zone validation', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + const domainName = Token.asString({ Ref: 'CertificateDomainName' }); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName, + hostedZonesByDomain: { [domainName]: hostedZone }, + }), + ).toThrow(/hostedZonesByDomain requires concrete domain names/); + }); + + test('rejects an unused multi-zone mapping', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZonesByDomain: { + 'api.example.com': hostedZone, + 'www.example.com': hostedZone, + }, + }), + ).toThrow(/mapping for "api\.example\.com", which is not a certificate domain name/); + }); + + test('rejects duplicate certificate names before default template validation', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone, + subjectAlternativeNames: ['WWW.EXAMPLE.COM.'], + }), + ).toThrow(/certificate domain names must be unique/); + }); +}); + +describe('certificate options, tags, metrics, and lifecycle', () => { + test('passes native ACM certificate options through', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + allowExport: true, + certificateName: 'Edge certificate', + domainName: 'test.example.com', + hostedZone, + keyAlgorithm: KeyAlgorithm.EC_PRIME256V1, + transparencyLoggingEnabled: false, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { + CertificateExport: 'ENABLED', + CertificateTransparencyLoggingPreference: 'DISABLED', + KeyAlgorithm: 'EC_prime256v1', + Tags: Match.arrayWith([{ Key: 'Name', Value: 'Edge certificate' }]), + }); + }); + + test('applies explicit tags in a generated stack', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + Tags.of(certificate).add('application', 'edge'); + Tags.of(certificate).add('costCenter', 'web'); + + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + Tags: Match.arrayWith([ + { Key: 'application', Value: 'edge' }, + { Key: 'costCenter', Value: 'web' }, + ]), + }); + }); + + test('proxies Tags.of() directly to a cross-region certificate', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + Tags.of(certificate).add('application', 'edge'); + + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + Tags: Match.arrayWith([{ Key: 'application', Value: 'edge' }]), + }); + }); + + test('proxies containing-stack tag aspects to a cross-region certificate', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + Tags.of(stack).add('environment', 'production'); + + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + Tags: Match.arrayWith([{ Key: 'environment', Value: 'production' }]), + }); + }); + + test('explicit certificateName takes precedence over a Name tag', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + certificateName: 'Friendly certificate', + domainName: 'test.example.com', + hostedZone, + }); + Tags.of(certificate).add('Name', 'Ignored name', { priority: 50 }); + Tags.of(certificate).add('application', 'edge'); + + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + Tags: Match.arrayWith([ + { Key: 'application', Value: 'edge' }, + { Key: 'Name', Value: 'Friendly certificate' }, + ]), + }); + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + Tags: Match.not(Match.arrayWith([{ Key: 'Name', Value: 'Ignored name' }])), + }); + }); + + test('a Name tag can replace the generated default name', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + Tags.of(certificate).add('Name', 'Tagged name', { priority: 200 }); + + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + Tags: Match.arrayWith([{ Key: 'Name', Value: 'Tagged name' }]), + }); + }); + + test('applies removal policy from props and through the method', () => { + const { stack, hostedZone } = crossRegionFixture(); + const retained = new DnsValidatedCertificateV2(stack, 'Retained', { + certificateRegion: 'us-east-1', + domainName: 'retained.example.com', + hostedZone, + removalPolicy: RemovalPolicy.RETAIN, + }); + const method = new DnsValidatedCertificateV2(stack, 'Method', { + certificateRegion: 'us-east-1', + domainName: 'method.example.com', + hostedZone, + }); + method.applyRemovalPolicy(RemovalPolicy.RETAIN); + + Template.fromStack(retained.certificateStack).resourceCountIs('AWS::CertificateManager::Certificate', 2); + const resources = Template.fromStack(retained.certificateStack).findResources( + 'AWS::CertificateManager::Certificate', + ); + expect(Object.values(resources).every(resource => resource.DeletionPolicy === 'Retain')).toBe(true); + }); + + test('metricDaysToExpiry uses the certificate region', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + expect(stack.resolve(certificate.metricDaysToExpiry().toMetricConfig())).toEqual({ + metricStat: expect.objectContaining({ + dimensions: [{ name: 'CertificateArn', value: stack.resolve(certificate.certificateArn) }], + metricName: 'DaysToExpiry', + namespace: 'AWS/CertificateManager', + period: stack.resolve(Duration.days(1)), + region: 'us-east-1', + regionOverride: 'us-east-1', + statistic: 'Minimum', + }), + renderingProperties: expect.anything(), + }); + }); + + test('supports cross-copy type detection', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }); + + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2(certificate)).toBe(true); + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2({})).toBe(false); + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2(null)).toBe(false); + }); +}); + +describe('invalid stack topology', () => { + test.each(['us-east-1', 'eu-west-1'])( + 'rejects certificateRegion %s together with certificateStack', + certificateRegion => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const certificateStack = createStack(app, 'Certificates', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateStack, + domainName: 'test.example.com', + hostedZone, + certificateRegion, + }), + ).toThrow(/specify at most one of certificateStack and certificateRegion/); + }, + ); + + test.each(['eu-west-1', 'us-east-1'])('infers explicit owner region %s', region => { + const { app, stack, hostedZone } = crossRegionFixture(); + const owner = createStack(app, 'Owner', region); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'test.example.com', + hostedZone, + certificateStack: owner, + }); + expect(certificate.certificateRegion).toBe(region); + expect(certificate.env.region).toBe(region); + Template.fromStack(owner).resourceCountIs('AWS::CertificateManager::Certificate', 1); + }); + + test('rejects a certificate stack in a different account', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const certificateStack = new Stack(app, 'Certificates', { + env: { account: OTHER_ACCOUNT, region: 'us-east-1' }, + }); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateStack, + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/must be in account "111111111111", got "222222222222"/); + }); + + test('rejects a certificate stack in a different stage', () => { + const app = createApp(); + const sourceStage = new Stage(app, 'Source'); + const certificateStage = new Stage(app, 'Certificates'); + const stack = createStack(sourceStage, 'Stack', 'eu-west-1'); + const certificateStack = createStack(certificateStage, 'CertificateStack', 'us-east-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateStack, + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/same CDK App or Stage/); + }); + + test('rejects a cross-partition certificate reference', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'cn-north-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/cross-partition references are not supported/); + }); + + test('rejects an unresolved certificate region', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'test.example.com', + hostedZone, + certificateRegion: Token.asString({ Ref: 'CertificateRegion' }), + }), + ).toThrow(/certificateRegion must be concrete/); + }); + + test('rejects an unresolved containing-stack region for a separate stack', () => { + const app = createApp(); + const stack = new Stack(app, 'Stack'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/containing stack to have a concrete region/); + }); + + test('rejects an explicit environment-agnostic certificate stack for a concrete account', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const certificateStack = new Stack(app, 'Certificates', { + env: { region: 'us-east-1' }, + }); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateStack, + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/must be in account/); + }); + + test('rejects a generated stack ID occupied by a non-stack construct', () => { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + new Construct(app, `dns-validated-certificate-stack-${stack.node.addr}-us-east-1`); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'test.example.com', + hostedZone, + certificateRegion: 'us-east-1', + }), + ).toThrow(/already exists in the stage and is not a Stack/); + }); + + test('requires an App or Stage when it needs to generate a stack', () => { + const root = new Construct(undefined as never, 'Root'); + const stack = new Stack(root, 'Stack', { + env: { account: ACCOUNT, region: 'eu-west-1' }, + }); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZone, + }), + ).toThrow(/inside a CDK App or Stage/); + }); +}); + +describe('OCF contract regressions', () => { + test('recognizes another loaded OCF copy without relying on instanceof', async () => { + let OtherCertificate: typeof DnsValidatedCertificateV2; + await jest.isolateModulesAsync(async () => { + OtherCertificate = (await import('../../src/aws-certificatemanager')).DnsValidatedCertificateV2; + }); + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new OtherCertificate!(stack, 'OtherCertificate', { domainName: 'www.example.com', hostedZone }); + expect(certificate instanceof DnsValidatedCertificateV2).toBe(false); + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2(certificate)).toBe(true); + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2(undefined)).toBe(false); + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2('certificate')).toBe(false); + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2({ [Symbol.for('unrelated.Certificate')]: true })).toBe( + false, + ); + }); + + test('retains explicit owners and preserves period and label metric overrides', () => { + const app = createApp(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const consumer = createStack(app, 'Consumer', 'eu-central-1'); + const zone = HostedZone.fromHostedZoneId(owner, 'Zone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(consumer, 'Certificate', { + domainName: 'www.example.com', + hostedZone: zone, + certificateStack: owner, + removalPolicy: RemovalPolicy.RETAIN, + }); + Template.fromStack(owner).hasResource('AWS::CertificateManager::Certificate', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }); + const metric = certificate.metricDaysToExpiry({ period: Duration.hours(6), label: 'Expiry' }); + expect(metric.period.toSeconds()).toBe(21600); + expect(metric.label).toBe('Expiry'); + expect(metric.statistic).toBe('Minimum'); + expect(metric.region).toBe('us-east-1'); + }); + + test.each([undefined, 'strong', 'weak'])( + 'wires the real producer output with global reference default %s', + strength => { + const app = new App({ context: strength ? { '@aws-cdk/core:defaultCrossStackReferences': strength } : {} }); + const stack = createStack(app, 'Consumer', 'eu-central-1'); + const zone = HostedZone.fromHostedZoneAttributes(stack, 'Zone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone: zone, + }); + new Distribution(stack, 'Distribution', { + certificate, + domainNames: ['www.example.com'], + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + expect(certificate.env.region).toBe('us-east-1'); + expect(certificate.env.account).toBe(ACCOUNT); + expect(certificate.stack).toBe(stack); + expect(Stack.of(certificate)).toBe(stack); + const assembly = app.synth(); + const consumer = assembly.getStackArtifact(stack.artifactId); + const owner = assembly.getStackArtifact(certificate.certificateStack.artifactId); + const distribution: any = Object.values(consumer.template.Resources).find( + (r: any) => r.Type === 'AWS::CloudFront::Distribution', + ); + const reference = + distribution.Properties.DistributionConfig.ViewerCertificate.AcmCertificateArn['Fn::GetStackOutput']; + expect(reference).toEqual({ + StackName: certificate.certificateStack.stackName, + OutputName: expect.any(String), + Region: 'us-east-1', + }); + const resource = certificate.node.defaultChild as CfnCertificate; + expect(owner.template.Outputs[reference.OutputName]).toEqual({ + Value: { Ref: certificate.certificateStack.getLogicalId(resource) }, + }); + expect(consumer.dependencies.map(d => d.id)).toContain(owner.id); + for (const artifact of assembly.stacks) { + for (const entry of Object.values(artifact.template.Resources ?? {}) as any[]) { + expect(entry.Type).not.toMatch( + /^(AWS::Lambda::Function|AWS::IAM::Role|AWS::IAM::Policy|AWS::Logs::LogGroup|AWS::CloudFormation::CustomResource|Custom::)/, + ); + } + } + }, + ); + + test.each([false, true])('exposes a truthful native default child, separate=%s', separate => { + const app = createApp(); + const stack = createStack(app, 'Consumer', separate ? 'eu-central-1' : 'us-east-1'); + const zone = HostedZone.fromHostedZoneId(stack, 'Zone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone: zone, + }); + const resource = certificate.node.defaultChild as CfnCertificate; + expect(CfnCertificate.isCfnCertificate(resource)).toBe(true); + expect(resource.node.id).toBe('Resource'); + expect(Stack.of(resource)).toBe(certificate.certificateStack); + resource.addPropertyOverride('CertificateTransparencyLoggingPreference', 'DISABLED'); + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + CertificateTransparencyLoggingPreference: 'DISABLED', + }); + Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', separate ? 0 : 1); + }); + + test('normalizes native names and exact multi-zone options without mutating inputs', () => { + const { stack, hostedZone } = crossRegionFixture(); + const sans = Object.freeze(['*.Example.COM.', 'Api.Example.COM.']); + const mappings = Object.freeze({ + 'EXAMPLE.COM.': hostedZone, + '*.example.com': hostedZone, + 'API.EXAMPLE.COM': hostedZone, + }); + const props = Object.freeze({ + domainName: 'Example.COM.', + subjectAlternativeNames: [...sans], + hostedZonesByDomain: mappings, + }); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', props); + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainName: 'example.com', + SubjectAlternativeNames: ['*.example.com', 'api.example.com'], + DomainValidationOptions: [ + { DomainName: 'example.com', HostedZoneId: 'Z123456' }, + { DomainName: 'api.example.com', HostedZoneId: 'Z123456' }, + ], + }); + expect(props.domainName).toBe('Example.COM.'); + expect(props.subjectAlternativeNames).toEqual(sans); + expect(Object.keys(mappings)).toEqual(['EXAMPLE.COM.', '*.example.com', 'API.EXAMPLE.COM']); + }); + + test('ignores inherited mappings and rejects an empty map before creating an owner', () => { + for (const inherited of [false, true]) { + const { app, stack, hostedZone } = crossRegionFixture(); + const hostedZonesByDomain = inherited ? Object.create({ 'test.example.com': hostedZone }) : {}; + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'test.example.com', + hostedZonesByDomain, + }), + ).toThrow(/Stack\/Certificate: hostedZonesByDomain must contain a mapping/); + expect(app.node.children.filter(Stack.isStack)).toEqual([stack]); + } + }); + + test.each([false, true])('rejects a known private zone before allocating support infrastructure, multi=%s', multi => { + const app = createApp(); + const stack = createStack(app, 'Consumer', 'us-east-1'); + const vpc = new Vpc(stack, 'Vpc', { maxAzs: 2, natGateways: 0 }); + const zone = new PrivateHostedZone(stack, 'Zone', { zoneName: 'example.com', vpc }); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + ...(multi ? { hostedZonesByDomain: { 'www.example.com': zone } } : { hostedZone: zone }), + }), + ).toThrow(/Consumer\/Certificate: hosted zone .* is private/); + expect(app.node.children.filter(Stack.isStack)).toEqual([stack]); + }); + + test('unknown imported zone privacy and tokenized zone privacy remain caller preconditions', () => { + const app = createApp(); + const stack = createStack(app, 'Consumer', 'us-east-1'); + const zone = new PublicHostedZone(stack, 'Zone', { zoneName: 'example.com' }); + (zone.node.defaultChild as CfnHostedZone).vpcs = Lazy.any({ produce: () => undefined }); + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone: zone, + }); + Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 1); + }); + + test('preserves unresolved single-zone names and SAN lists', () => { + const app = createApp(); + const stack = createStack(app, 'Consumer', 'us-east-1'); + const zone = HostedZone.fromHostedZoneId(stack, 'Zone', 'Z123456'); + const domainName = Lazy.string({ produce: () => 'www.example.com' }); + const subjectAlternativeNames = Lazy.list({ produce: () => ['api.example.com'] }); + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName, + subjectAlternativeNames, + hostedZone: zone, + }); + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainName: 'www.example.com', + SubjectAlternativeNames: ['api.example.com'], + }); + }); + + test('authority checks respect DNS label boundaries', () => { + const app = createApp(); + const stack = createStack(app, 'Consumer', 'us-east-1'); + const zone = HostedZone.fromHostedZoneAttributes(stack, 'Zone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'notexample.com', + hostedZone: zone, + }), + ).toThrow(/not authoritative/); + }); + + test.each(['duplicate', 'authority', 'opaque', 'valid'])('checks lazy SAN lists at synthesis: %s', kind => { + const app = createApp(); + const stack = createStack(app, 'Consumer', 'eu-central-1'); + const zone = HostedZone.fromHostedZoneAttributes(stack, 'Zone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + let names: string[] = []; + const subjectAlternativeNames = + kind === 'opaque' + ? new CfnParameter(stack, 'Names', { type: 'CommaDelimitedList' }).valueAsList + : Lazy.list({ produce: () => names }); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone: zone, + subjectAlternativeNames, + }); + names = + kind === 'duplicate' + ? ['WWW.EXAMPLE.COM.'] + : kind === 'authority' + ? ['notexample.com'] + : ['api.example.com', '*.example.com', 'example.com']; + if (kind === 'valid') { + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + SubjectAlternativeNames: names, + DomainValidationOptions: ['www.example.com', 'api.example.com', 'example.com'].map(DomainName => ({ + DomainName, + HostedZoneId: 'Z123456', + })), + }); + } else { + expect(() => app.synth()).toThrow( + kind === 'duplicate' + ? /must be unique/ + : kind === 'authority' + ? /not authoritative/ + : /must resolve to a fixed-length list at synthesis/, + ); + } + }); + + test.each([false, true])('forwards app, parent, direct tags and removal with precedence, separate=%s', separate => { + const app = createApp(); + const stack = createStack(app, 'Consumer', separate ? 'eu-central-1' : 'us-east-1'); + const scope = new Construct(stack, 'Scope'); + const zone = HostedZone.fromHostedZoneId(stack, 'Zone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(scope, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone: zone, + + certificateName: 'named', + }); + Tags.of(app).add('App', 'app'); + Tags.of(scope).add('Parent', 'parent'); + Tags.of(certificate).add('Direct', 'direct'); + Tags.of(certificate).add('Priority', 'aspect', { priority: 200 }); + Tags.of(certificate).add('Removed', 'remove-me'); + Tags.of(certificate).remove('Removed'); + Tags.of(certificate).add('Excluded', 'no', { excludeResourceTypes: ['AWS::CertificateManager::Certificate'] }); + const resources = Template.fromStack(certificate.certificateStack).findResources( + 'AWS::CertificateManager::Certificate', + ); + const tags = Object.values(resources)[0].Properties.Tags; + expect(tags).toEqual([ + { Key: 'App', Value: 'app' }, + { Key: 'Direct', Value: 'direct' }, + { Key: 'Name', Value: 'named' }, + { Key: 'Parent', Value: 'parent' }, + { Key: 'Priority', Value: 'aspect' }, + ]); + }); + + test('keeps automatic owners separate by containing stack and target region', () => { + const app = createApp(); + const first = createStack(app, 'First', 'eu-central-1'); + const second = createStack(app, 'Second', 'eu-central-1'); + const zone = HostedZone.fromHostedZoneId(first, 'Zone', 'Z123456'); + const a = new DnsValidatedCertificateV2(first, 'A', { + certificateRegion: 'us-east-1', + domainName: 'a.example.com', + hostedZone: zone, + }); + const b = new DnsValidatedCertificateV2(second, 'B', { + certificateRegion: 'us-east-1', + domainName: 'b.example.com', + hostedZone: zone, + }); + const c = new DnsValidatedCertificateV2(first, 'C', { + domainName: 'c.example.com', + hostedZone: zone, + certificateRegion: 'eu-west-1', + }); + expect(new Set([a.certificateStack, b.certificateStack, c.certificateStack]).size).toBe(3); + const before = app.synth().stacks.map(s => [s.id, s.template]); + expect(app.synth().stacks.map(s => [s.id, s.template])).toEqual(before); + }); + + test('supports explicit same-stack and shared owners with direct native references', () => { + const app = createApp(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const consumer = createStack(app, 'Consumer', 'eu-central-1'); + const zone = HostedZone.fromHostedZoneId(owner, 'Zone', 'Z123456'); + const a = new DnsValidatedCertificateV2(owner, 'A', { + domainName: 'a.example.com', + hostedZone: zone, + certificateStack: owner, + }); + const b = new DnsValidatedCertificateV2(consumer, 'B', { + domainName: 'b.example.com', + hostedZone: zone, + certificateStack: owner, + }); + new Distribution(owner, 'Distribution', { + certificate: a, + domainNames: ['a.example.com'], + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + new Distribution(consumer, 'Distribution', { + certificate: b, + domainNames: ['b.example.com'], + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + const template = Template.fromStack(owner); + template.resourceCountIs('AWS::CertificateManager::Certificate', 2); + template.hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: { + ViewerCertificate: { + AcmCertificateArn: { Ref: owner.getLogicalId(a.node.defaultChild as CfnCertificate) }, + SslSupportMethod: 'sni-only', + }, + }, + }); + }); + + test('rejects an unmanaged stack collision instead of adopting it', () => { + const { app, stack, hostedZone } = crossRegionFixture(); + createStack(app, `dns-validated-certificate-stack-${stack.node.addr}-us-east-1`, 'us-east-1'); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'a.example.com', + hostedZone, + }), + ).toThrow(/not a generated certificate owner; pass it as certificateStack/); + }); + + test('a nested consumer depends on a top-level regional owner through its parent', () => { + const app = createApp(); + const parent = createStack(app, 'Parent', 'eu-central-1'); + const nested = new NestedStack(parent, 'Nested'); + const zone = HostedZone.fromHostedZoneId(nested, 'Zone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(nested, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone: zone, + }); + new Distribution(nested, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + Template.fromStack(nested).hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: { + ViewerCertificate: { AcmCertificateArn: weakCertificateArnReference(nested), SslSupportMethod: 'sni-only' }, + }, + }); + expect( + app + .synth() + .getStackArtifact(parent.artifactId) + .dependencies.map(d => d.id), + ).toContain(certificate.certificateStack.artifactId); + }); + + test('a parent consumes a nested owner through native nested outputs', () => { + const app = createApp(); + const parent = createStack(app, 'Parent', 'us-east-1'); + const owner = new NestedStack(parent, 'Owner'); + const zone = HostedZone.fromHostedZoneId(owner, 'Zone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(parent, 'Certificate', { + domainName: 'www.example.com', + hostedZone: zone, + certificateStack: owner, + }); + new Distribution(parent, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + const template = Template.fromStack(parent).toJSON(); + const distribution: any = Object.values(template.Resources).find( + (r: any) => r.Type === 'AWS::CloudFront::Distribution', + ); + const reference = distribution.Properties.DistributionConfig.ViewerCertificate.AcmCertificateArn; + expect(reference['Fn::GetStackOutput']).toBeUndefined(); + const [id, output] = reference['Fn::GetAtt']; + expect(template.Resources[id].Type).toBe('AWS::CloudFormation::Stack'); + expect(Template.fromStack(owner).toJSON().Outputs[output.replace(/^Outputs\./, '')].Value).toEqual({ + Ref: owner.getLogicalId(certificate.node.defaultChild as CfnCertificate), + }); + }); + + test('creates native same-stack certificates within nested stacks', () => { + const app = createApp(); + const parent = createStack(app, 'Parent', 'us-east-1'); + const nested = new NestedStack(parent, 'Nested'); + const zone = new PublicHostedZone(nested, 'Zone', { zoneName: 'example.com' }); + const certificate = new DnsValidatedCertificateV2(nested, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone: zone, + }); + expect(certificate.certificateStack).toBe(nested); + Template.fromStack(nested).resourceCountIs('AWS::CertificateManager::Certificate', 1); + Template.fromStack(parent).resourceCountIs('AWS::CertificateManager::Certificate', 0); + }); + + test('attributes imports remain ordinary ACM interfaces and create no owner', () => { + const app = createApp(); + const stack = createStack(app, 'Consumer', 'eu-central-1'); + const arn = `arn:aws:acm:us-east-1:${ACCOUNT}:certificate/12345678-1234-1234-1234-123456789012`; + const certificate = DnsValidatedCertificateV2.fromCertificateAttributes(stack, 'Imported', { certificateArn: arn }); + new Distribution(stack, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + expect(certificate.certificateArn).toBe(arn); + expect(DnsValidatedCertificateV2.isDnsValidatedCertificateV2(certificate)).toBe(false); + expect(app.node.children.filter(Stack.isStack)).toEqual([stack]); + Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 0); + }); + + test('legacy CloudFront also accepts the native regional reference', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.example.com', + hostedZone, + }); + new CloudFrontWebDistribution(stack, 'Distribution', { + viewerCertificate: ViewerCertificate.fromAcmCertificate(certificate, { aliases: ['www.example.com'] }), + originConfigs: [ + { customOriginSource: { domainName: 'origin.example.com' }, behaviors: [{ isDefaultBehavior: true }] }, + ], + }); + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: { + ViewerCertificate: Match.objectLike({ AcmCertificateArn: weakCertificateArnReference(stack) }), + }, + }); + }); +}); + +function createApp(): App { + const app = new App({ context: WEAK_REFERENCE_CONTEXT }); + Validations.of(app).acknowledge({ + id: 'CloudFormation-Validate::F0001', + reason: 'A weak-reference consumer can intentionally contain only outputs in unit tests.', + }); + return app; +} + +function createStack(scope: Construct, id: string, region: string): Stack { + return new Stack(scope, id, { + env: { account: ACCOUNT, region }, + }); +} + +function crossRegionFixture(): { + readonly app: App; + readonly stack: Stack; + readonly hostedZone: ReturnType; +} { + const app = createApp(); + const stack = createStack(app, 'Stack', 'eu-west-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'HostedZone', 'Z123456'); + return { app, stack, hostedZone }; +} + +function generatedCertificateStack(app: App, containingStack: Stack, region = 'us-east-1'): Stack { + return app.node.findChild(`dns-validated-certificate-stack-${containingStack.node.addr}-${region}`) as Stack; +} + +function weakCertificateArnReference( + containingStack: Stack, + stackNamePattern = `dns-validated-certificate-stack-${containingStack.node.addr}-us-east-1`, + region = 'us-east-1', +): unknown { + return { + 'Fn::GetStackOutput': { + OutputName: Match.anyValue(), + Region: region, + StackName: Match.stringLikeRegexp(stackNamePattern), + }, + }; +} + +describe('revised placement and late DNS contracts', () => { + test.each([false, true])('omitted placement stays local, environment agnostic=%s', agnostic => { + const app = createApp(); + const stack = agnostic ? new Stack(app, 'Consumer') : createStack(app, 'Consumer', 'eu-central-1'); + const hostedZone = HostedZone.fromHostedZoneId(stack, 'Zone', 'Z123456'); + for (const explicit of [false, true]) { + const certificate = new DnsValidatedCertificateV2(stack, explicit ? 'Explicit' : 'Default', { + domainName: 'www.example.com', + hostedZone, + ...(explicit ? { certificateStack: stack } : {}), + }); + expect(certificate.certificateStack).toBe(stack); + expect(certificate.certificateRegion).toBe(stack.region); + expect(certificate.env.region).toBe(stack.region); + } + Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 2); + expect(app.synth().stacks).toHaveLength(1); + }); + + test.each([false, true])('native owner-local hosted zone succeeds, map=%s', multi => { + const { app, stack } = crossRegionFixture(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const zone = new PublicHostedZone(owner, 'Zone', { zoneName: 'example.com' }); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + certificateStack: owner, + ...(multi ? { hostedZonesByDomain: { 'www.example.com': zone } } : { hostedZone: zone }), + }); + new Distribution(stack, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + const template = Template.fromStack(owner); + template.hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainValidationOptions: [ + { + DomainName: 'www.example.com', + HostedZoneId: { Ref: owner.getLogicalId(zone.node.defaultChild as CfnHostedZone) }, + }, + ], + }); + expect(owner.dependencies).not.toContain(stack); + expect(stack.dependencies).toContain(owner); + expect(owner.node.children.filter(child => child.node.id.startsWith('ValidationZone'))).toHaveLength(0); + }); + + test('an imported token scoped under the owner does not establish native zone ownership', () => { + const { app, stack } = crossRegionFixture(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const foreignId = new CfnParameter(stack, 'ZoneId').valueAsString; + const zone = HostedZone.fromHostedZoneId(owner, 'Imported', foreignId); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone: zone, + certificateStack: owner, + }), + ).toThrow(/require concrete hosted zone IDs or a native hosted zone in certificateStack/); + }); + + test.each([false, true])('domain parameter belongs in native owner, foreign=%s', foreign => { + const { app, stack, hostedZone } = crossRegionFixture(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const parameter = new CfnParameter(foreign ? stack : owner, 'DomainName'); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: parameter.valueAsString, + hostedZone, + certificateStack: owner, + }); + new Distribution(stack, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + if (foreign) { + expect(() => app.synth()).toThrow(/cyclic reference/); + } else { + Template.fromStack(owner).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainName: { Ref: 'DomainName' }, + DomainValidationOptions: [{ DomainName: { Ref: 'DomainName' }, HostedZoneId: 'Z123456' }], + }); + } + }); + + test('eager and late normalized names emit identical properties after caller configuration', () => { + const properties = (late: boolean) => { + const { stack, hostedZone } = crossRegionFixture(); + let values: string[] = []; + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: late ? Lazy.string({ produce: () => 'WWW.Example.COM.' }) : 'www.example.com', + subjectAlternativeNames: late + ? Lazy.list({ produce: () => values }) + : ['api.example.com', '*.example.com', 'example.com'], + hostedZone, + }); + values = ['Api.Example.COM.', '*.Example.COM.', 'Example.COM.']; + return Object.values(Template.fromStack(stack).findResources('AWS::CertificateManager::Certificate'))[0] + .Properties; + }; + expect(properties(true)).toEqual(properties(false)); + }); + + test.each(['duplicate', 'authority'])('late normalized %s names fail', kind => { + const { app, stack } = crossRegionFixture(); + const hostedZone = HostedZone.fromHostedZoneAttributes(stack, 'KnownZone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + subjectAlternativeNames: Lazy.list({ + produce: () => [kind === 'duplicate' ? 'WWW.Example.COM.' : 'Api.Other.COM.'], + }), + }); + expect(() => app.synth()).toThrow(kind === 'duplicate' ? /must be unique/ : /not authoritative/); + }); + + test('a real subclass uses the public native handle and retains lazy DNS validation', () => { + class ExtendedCertificate extends DnsValidatedCertificateV2 { + protected createCertificateResource( + scope: Construct, + id: string, + props: import('aws-cdk-lib/aws-certificatemanager').CertificateProps, + ): CfnCertificate { + const resource = super.createCertificateResource(scope, id, props); + resource.addPropertyOverride('CertificateTransparencyLoggingPreference', 'DISABLED'); + return resource; + } + } + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new ExtendedCertificate(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + certificateRegion: 'us-east-1', + subjectAlternativeNames: Lazy.list({ produce: () => ['API.Example.COM.'] }), + }); + expect(certificate.certificateResource).toBe(certificate.node.defaultChild); + expect(Stack.of(certificate.certificateResource)).toBe(certificate.certificateStack); + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + CertificateTransparencyLoggingPreference: 'DISABLED', + SubjectAlternativeNames: ['api.example.com'], + }); + Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 0); + }); + + test('generated owners are shared across independently loaded package copies', async () => { + let OtherCertificate: typeof DnsValidatedCertificateV2; + await jest.isolateModulesAsync(async () => { + OtherCertificate = (await import('../../src/aws-certificatemanager')).DnsValidatedCertificateV2; + }); + const { stack, hostedZone } = crossRegionFixture(); + const first = new DnsValidatedCertificateV2(stack, 'First', { + domainName: 'a.example.com', + hostedZone, + certificateRegion: 'us-east-1', + }); + const second = new OtherCertificate!(stack, 'Second', { + domainName: 'b.example.com', + hostedZone, + certificateRegion: 'us-east-1', + }); + expect(first.certificateStack).toBe(second.certificateStack); + Template.fromStack(first.certificateStack).resourceCountIs('AWS::CertificateManager::Certificate', 2); + }); +}); + +test('rejects a separate owner with unresolved region before allocating native resources', () => { + const { app, stack, hostedZone } = crossRegionFixture(); + const owner = new Stack(app, 'Owner'); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + certificateStack: owner, + }), + ).toThrow(/separate certificateStack must have a concrete region/); + expect(owner.node.children).toHaveLength(0); +}); + +test('caches a shared imported zone for exact multi-zone mappings', () => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + subjectAlternativeNames: ['api.example.com'], + certificateRegion: 'us-east-1', + hostedZonesByDomain: { 'www.example.com': hostedZone, 'api.example.com': hostedZone }, + }); + expect( + certificate.certificateStack.node.children.filter(child => child.node.id.startsWith('ValidationZone')), + ).toHaveLength(1); + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainValidationOptions: [ + { DomainName: 'www.example.com', HostedZoneId: 'Z123456' }, + { DomainName: 'api.example.com', HostedZoneId: 'Z123456' }, + ], + }); +}); + +test('rejects a native private zone even when it belongs to the explicit owner', () => { + const { app, stack } = crossRegionFixture(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const zone = new PrivateHostedZone(owner, 'Zone', { + zoneName: 'example.com', + vpc: new Vpc(owner, 'Vpc', { natGateways: 0 }), + }); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone: zone, + certificateStack: owner, + }), + ).toThrow(/is private/); +}); + +test.each([false, true])('scalar token SAN arrays retain source dependencies, lazy=%s', lazy => { + const { app, stack, hostedZone } = crossRegionFixture(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const parameter = new CfnParameter(stack, 'ForeignSan'); + const sans = [parameter.valueAsString]; + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + certificateStack: owner, + subjectAlternativeNames: lazy ? Lazy.list({ produce: () => sans }) : sans, + }); + new Distribution(stack, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + expect(() => app.synth()).toThrow(/cyclic reference/); +}); + +test('explicit owner tag aspects reach the native certificate alongside wrapper tags', () => { + const { app, stack, hostedZone } = crossRegionFixture(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + certificateStack: owner, + }); + Tags.of(owner).add('Owner', 'certificates'); + Tags.of(certificate).add('Service', 'web'); + Template.fromStack(owner).hasResourceProperties('AWS::CertificateManager::Certificate', { + Tags: Match.arrayWith([ + { Key: 'Owner', Value: 'certificates' }, + { Key: 'Service', Value: 'web' }, + ]), + }); +}); + +test.each([false, true])('owner-local scalar SAN tokens synthesize without flattening, lazy=%s', lazy => { + const { app, stack, hostedZone } = crossRegionFixture(); + const owner = createStack(app, 'Owner', 'us-east-1'); + const parameter = new CfnParameter(owner, 'San'); + const sans = [parameter.valueAsString]; + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + certificateStack: owner, + subjectAlternativeNames: lazy ? Lazy.list({ produce: () => sans }) : sans, + }); + Template.fromStack(owner).hasResourceProperties('AWS::CertificateManager::Certificate', { + SubjectAlternativeNames: [{ Ref: 'San' }], + DomainValidationOptions: [ + { DomainName: 'www.example.com', HostedZoneId: 'Z123456' }, + { DomainName: { Ref: 'San' }, HostedZoneId: 'Z123456' }, + ], + }); + expect(owner.dependencies).not.toContain(stack); +}); + +test('rejects a known authority mismatch before creating a generated owner', () => { + const { app, stack } = crossRegionFixture(); + const hostedZone = HostedZone.fromHostedZoneAttributes(stack, 'KnownZone', { + hostedZoneId: 'Z123456', + zoneName: 'example.com', + }); + expect( + () => + new DnsValidatedCertificateV2(stack, 'Certificate', { + certificateRegion: 'us-east-1', + domainName: 'www.other.com', + hostedZone, + }), + ).toThrow(/not authoritative/); + expect(app.node.children.filter(Stack.isStack)).toEqual([stack]); +}); + +describe('producer reference strength across shared consumption contexts', () => { + const cases = [undefined, 'strong', 'weak', 'both'].flatMap(policy => + [false, true].flatMap(local => [false, true].map(reverse => ({ policy, local, reverse }))), + ); + test.each(cases)('keeps references contextual: %j', ({ policy, local, reverse }) => { + const app = new App({ context: policy ? { '@aws-cdk/core:defaultCrossStackReferences': policy } : {} }); + const stackSpecs = [ + ['Owner', 'us-east-1'], + ['Consumer', 'eu-central-1'], + ['SecondConsumer', 'eu-west-1'], + ]; + const stacks = new Map( + (reverse ? [...stackSpecs].reverse() : stackSpecs).map(([id, region]) => [id, createStack(app, id, region)]), + ); + const owner = stacks.get('Owner')!; + const consumer = stacks.get('Consumer')!; + const second = stacks.get('SecondConsumer')!; + const nested = new NestedStack(owner, 'NestedConsumer'); + const hostedZone = HostedZone.fromHostedZoneId(owner, 'Zone', 'Z123456'); + const certificate = new DnsValidatedCertificateV2(local ? owner : consumer, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + ...(local ? {} : { certificateStack: owner }), + }); + new CfnOutput(owner, 'CertificateArn', { value: certificate.certificateArn }); + new CfnOutput(owner, 'CertificateRefArn', { value: certificate.certificateRef.certificateArn }); + new Alarm(owner, 'Expiry', { metric: certificate.metricDaysToExpiry(), threshold: 30, evaluationPeriods: 1 }); + const consumers = [owner, consumer, second, nested]; + for (const scope of reverse ? [...consumers].reverse() : consumers) { + new Distribution(scope, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + } + const assembly = app.synth(); + const own = assembly.getStackArtifact(owner.artifactId).template; + const localRef = { Ref: owner.getLogicalId(certificate.certificateResource) }; + const viewerArn = (template: any): any => + Object.values(template.Resources) + .filter((resource: any) => resource.Type === 'AWS::CloudFront::Distribution') + .map((resource: any) => resource.Properties.DistributionConfig.ViewerCertificate.AcmCertificateArn)[0]; + expect(own.Outputs.CertificateArn.Value).toEqual(localRef); + expect(own.Outputs.CertificateRefArn.Value).toEqual(localRef); + expect(viewerArn(own)).toEqual(localRef); + Template.fromStack(owner).hasResourceProperties('AWS::CloudWatch::Alarm', { + Dimensions: [{ Name: 'CertificateArn', Value: localRef }], + }); + for (const remote of [consumer, second]) { + const artifact = assembly.getStackArtifact(remote.artifactId); + const reference = viewerArn(artifact.template)['Fn::GetStackOutput']; + expect(reference).toEqual({ StackName: owner.stackName, Region: 'us-east-1', OutputName: expect.any(String) }); + expect(own.Outputs[reference.OutputName].Value).toEqual(localRef); + expect(artifact.dependencies.map(dependency => dependency.id)).toContain(owner.artifactId); + } + const nestedTemplate = Template.fromStack(nested).toJSON(); + const parameter = viewerArn(nestedTemplate).Ref; + expect(nestedTemplate.Parameters[parameter].Type).toBe('String'); + expect(own.Resources[owner.getLogicalId(nested.nestedStackResource!)].Properties.Parameters[parameter]).toEqual( + localRef, + ); + expect(owner.dependencies).not.toContain(consumer); + expect(owner.dependencies).not.toContain(second); + for (const template of [...assembly.stacks.map(stack => stack.template), nestedTemplate]) { + for (const resource of Object.values(template.Resources ?? {}) as any[]) { + expect(resource.Type).not.toMatch( + /^(AWS::Lambda::|AWS::IAM::|AWS::Logs::|Custom::|AWS::CloudFormation::CustomResource)/, + ); + } + } + }); +}); + +describe('optional lazy SAN absence', () => { + const forms = { + omitted: () => undefined, + empty: () => [], + lazyEmpty: () => Lazy.list({ produce: () => [] }), + omitEmpty: () => Lazy.list({ produce: () => [] }, { omitEmpty: true }), + lazyUndefined: () => Lazy.list({ produce: () => undefined }), + }; + test.each( + Object.entries(forms).flatMap(([name, produce]) => [false, true].map(separate => ({ name, produce, separate }))), + )('omits native SANs for $name, separate=$separate', ({ produce, separate }) => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'WWW.Example.COM.', + hostedZone, + ...(separate ? { certificateRegion: 'us-east-1' } : {}), + subjectAlternativeNames: produce(), + }); + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainName: 'www.example.com', + SubjectAlternativeNames: Match.absent(), + DomainValidationOptions: [{ DomainName: 'www.example.com', HostedZoneId: 'Z123456' }], + }); + }); + + test('an omitEmpty producer can be populated after construction', () => { + const { stack, hostedZone } = crossRegionFixture(); + let names: string[] | undefined; + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + subjectAlternativeNames: Lazy.list({ produce: () => names }, { omitEmpty: true }), + }); + names = ['API.Example.COM.']; + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + SubjectAlternativeNames: ['api.example.com'], + DomainValidationOptions: [ + { DomainName: 'www.example.com', HostedZoneId: 'Z123456' }, + { DomainName: 'api.example.com', HostedZoneId: 'Z123456' }, + ], + }); + }); + + test.each(['split', 'condition'])('still rejects a deployment-time %s list', kind => { + const { app, stack, hostedZone } = crossRegionFixture(); + const value = new CfnParameter(stack, 'Names').valueAsString; + new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + hostedZone, + subjectAlternativeNames: + kind === 'split' ? Fn.split(',', value) : Token.asList(Fn.conditionIf('HasNames', ['api.example.com'], [])), + }); + expect(() => app.synth()).toThrow(/subjectAlternativeNames must resolve to a fixed-length list/); + }); +}); + +test.each([undefined, []])('omits empty concrete SANs in exact multi-zone mode: %j', subjectAlternativeNames => { + const { stack, hostedZone } = crossRegionFixture(); + const certificate = new DnsValidatedCertificateV2(stack, 'Certificate', { + domainName: 'www.example.com', + subjectAlternativeNames, + hostedZonesByDomain: { 'www.example.com': hostedZone }, + }); + Template.fromStack(certificate.certificateStack).hasResourceProperties('AWS::CertificateManager::Certificate', { + SubjectAlternativeNames: Match.absent(), + DomainValidationOptions: [{ DomainName: 'www.example.com', HostedZoneId: 'Z123456' }], + }); +}); + +describe('nested certificate owner reference boundary', () => { + const policies = [undefined, 'strong', 'weak', 'both']; + const boundaryError = + /nested stack cannot be consumed outside its top-level stack tree; use a top-level certificateStack/; + + test.each( + policies.flatMap(policy => + ['us-east-1', 'eu-central-1'].flatMap(region => + [false, true].flatMap(explicitOwner => + ['arn', 'typed', 'metric'].map(surface => ({ policy, region, explicitOwner, surface })), + ), + ), + ), + )('rejects external consumption: %j', ({ policy, region, explicitOwner, surface }) => { + const app = new App({ context: policy ? { '@aws-cdk/core:defaultCrossStackReferences': policy } : {} }); + const parent = createStack(app, 'Parent', 'us-east-1'); + const owner = new NestedStack(parent, 'Owner'); + const consumer = createStack(app, 'Consumer', region); + const certificate = new DnsValidatedCertificateV2(explicitOwner ? consumer : owner, 'Certificate', { + domainName: 'www.example.com', + hostedZone: HostedZone.fromHostedZoneId(owner, 'Zone', 'Z123456'), + ...(explicitOwner ? { certificateStack: owner } : {}), + }); + new CfnOutput(owner, 'LocalArn', { value: certificate.certificateArn }); + if (surface === 'metric') { + new Dashboard(consumer, 'Expiry', { widgets: [[new GraphWidget({ left: [certificate.metricDaysToExpiry()] })]] }); + } else { + new CfnOutput(consumer, 'Arn', { + value: surface === 'typed' ? certificate.certificateRef.certificateArn : certificate.certificateArn, + }); + } + expect(() => app.synth()).toThrow(boundaryError); + }); + + test.each( + policies.flatMap(policy => [1, 2].flatMap(depth => [false, true].map(reverse => ({ policy, depth, reverse })))), + )('preserves native tree wiring: %j', ({ policy, depth, reverse }) => { + const app = new App({ context: policy ? { '@aws-cdk/core:defaultCrossStackReferences': policy } : {} }); + const parent = createStack(app, 'Parent', 'us-east-1'); + const first = new NestedStack(parent, 'First'); + const owner = depth === 1 ? first : new NestedStack(first, 'Owner'); + const sibling = new NestedStack(parent, 'Sibling'); + const descendant = new NestedStack(owner, 'Descendant'); + // Wrapper placement must not determine the actual consumer's reference boundary. + const wrapper = createStack(app, 'Wrapper', 'us-east-1'); + const certificate = new DnsValidatedCertificateV2(wrapper, 'Certificate', { + domainName: 'www.example.com', + hostedZone: HostedZone.fromHostedZoneId(owner, 'Zone', 'Z123456'), + certificateStack: owner, + }); + const stacks = [...new Set([parent, first, owner, sibling, descendant])]; + for (const stack of reverse ? [...stacks].reverse() : stacks) { + new CfnOutput(stack, 'Arn', { value: certificate.certificateArn }); + new CfnOutput(stack, 'TypedArn', { value: certificate.certificateRef.certificateArn }); + new Distribution(stack, 'Distribution', { + certificate, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, + }); + new Alarm(stack, 'Expiry', { metric: certificate.metricDaysToExpiry(), threshold: 30, evaluationPeriods: 1 }); + } + app.synth(); + const templates = new Map(stacks.map(stack => [stack, Template.fromStack(stack).toJSON()])); + const nativeId = owner.getLogicalId(certificate.certificateResource); + + // Follow CDK's nested parameters and outputs all the way to the ACM resource. + function assertNativeReference(stack: Stack, value: any): void { + if (value.Ref === nativeId && stack === owner) { + return; + } + if (value.Ref !== undefined) { + expect(templates.get(stack)!.Parameters[value.Ref]).toBeDefined(); + const parentStack = stack.nestedStackParent!; + const resourceId = parentStack.getLogicalId(stack.nestedStackResource!); + assertNativeReference( + parentStack, + templates.get(parentStack)!.Resources[resourceId].Properties.Parameters[value.Ref], + ); + return; + } + expect(value['Fn::GetAtt']).toBeDefined(); + const [resourceId, output] = value['Fn::GetAtt']; + const child = stacks.find( + candidate => + candidate.nestedStackParent === stack && stack.getLogicalId(candidate.nestedStackResource!) === resourceId, + )!; + expect(child).toBeDefined(); + assertNativeReference(child, templates.get(child)!.Outputs[output.replace(/^Outputs\./, '')].Value); + } + for (const stack of stacks) { + const template = templates.get(stack)!; + assertNativeReference(stack, template.Outputs.Arn.Value); + expect(template.Outputs.TypedArn.Value).toEqual(template.Outputs.Arn.Value); + const resources: any[] = Object.values(template.Resources); + const distribution = resources.find(resource => resource.Type === 'AWS::CloudFront::Distribution'); + expect(distribution.Properties.DistributionConfig.ViewerCertificate.AcmCertificateArn).toEqual( + template.Outputs.Arn.Value, + ); + const alarm = resources.find(resource => resource.Type === 'AWS::CloudWatch::Alarm'); + expect(alarm.Properties.Dimensions).toContainEqual({ Name: 'CertificateArn', Value: template.Outputs.Arn.Value }); + expect(JSON.stringify(template)).not.toMatch( + /Fn::ImportValue|Fn::GetStackOutput|AWS::Lambda::|AWS::IAM::|AWS::Logs::|Custom::|AWS::CloudFormation::CustomResource/, + ); + for (const output of Object.values(template.Outputs) as any[]) { + expect(output.Export).toBeUndefined(); + } + } + }); + + test('does not change unrelated nested resource export strength', () => { + const app = new App({ context: { '@aws-cdk/core:defaultCrossStackReferences': 'strong' } }); + const parent = createStack(app, 'Parent', 'us-east-1'); + const owner = new NestedStack(parent, 'Owner'); + const external = createStack(app, 'External', 'us-east-1'); + const unrelated = new CfnHostedZone(owner, 'Unrelated', { name: 'unrelated.example.com' }); + const certificate = new DnsValidatedCertificateV2(owner, 'Certificate', { + domainName: 'www.example.com', + hostedZone: HostedZone.fromHostedZoneId(owner, 'Zone', 'Z123456'), + }); + new CfnOutput(parent, 'Arn', { value: certificate.certificateArn }); + new CfnOutput(external, 'ZoneId', { value: unrelated.ref }); + app.synth(); + const parentTemplate = Template.fromStack(parent).toJSON(); + const externalTemplate = Template.fromStack(external).toJSON(); + const exportName = externalTemplate.Outputs.ZoneId.Value['Fn::ImportValue']; + expect(exportName).toBeDefined(); + expect(Object.values(parentTemplate.Outputs)).toContainEqual( + expect.objectContaining({ + Export: { Name: exportName }, + }), + ); + expect(parentTemplate.Outputs.Arn.Export).toBeUndefined(); + }); +}); diff --git a/test/aws-certificatemanager/integ.dns-validated-certificate-v2.ts b/test/aws-certificatemanager/integ.dns-validated-certificate-v2.ts new file mode 100644 index 0000000..c5e197f --- /dev/null +++ b/test/aws-certificatemanager/integ.dns-validated-certificate-v2.ts @@ -0,0 +1,54 @@ +import { ExpectedResult, IntegTest } from '@aws-cdk/integ-tests-alpha'; +import { App, CfnOutput, ReferenceStrength, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { Distribution, PriceClass } from 'aws-cdk-lib/aws-cloudfront'; +import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'; +import { HostedZone } from 'aws-cdk-lib/aws-route53'; +import { aws_certificatemanager } from '../../src'; + +const app = new App({ context: { '@aws-cdk/core:defaultCrossStackReferences': 'strong' } }); +const account = process.env.CDK_DEFAULT_ACCOUNT; +const zoneId = process.env.OCF_CERTIFICATE_INTEG_ZONE_ID; +const zoneName = process.env.OCF_CERTIFICATE_INTEG_ZONE_NAME; +if (!zoneId || !zoneName) { + throw new Error( + 'set OCF_CERTIFICATE_INTEG_ZONE_ID and OCF_CERTIFICATE_INTEG_ZONE_NAME to the approved public DNS fixture', + ); +} +const domainName = `ocf-certificate-integ.${zoneName.toLowerCase().replace(/\.$/, '')}`; +const owner = new Stack(app, 'CertificateIntegOwner', { env: { account, region: 'us-east-1' } }); +const consumer = new Stack(app, 'CertificateIntegConsumer', { env: { account, region: 'eu-central-1' } }); +const zone = HostedZone.fromHostedZoneAttributes(owner, 'Zone', { hostedZoneId: zoneId, zoneName }); +const certificate = new aws_certificatemanager.DnsValidatedCertificateV2(consumer, 'Certificate', { + domainName, + hostedZone: zone, + // The explicit owner supplies its us-east-1 region. + certificateStack: owner, + allowExport: false, + removalPolicy: RemovalPolicy.DESTROY, +}); +const distribution = new Distribution(consumer, 'Distribution', { + certificate, + domainNames: [domainName], + priceClass: PriceClass.PRICE_CLASS_100, + defaultBehavior: { origin: new HttpOrigin('origin.example.com') }, +}); +new CfnOutput(consumer, 'CertificateArn', { value: certificate.certificateArn }); +new CfnOutput(consumer, 'DistributionId', { value: distribution.distributionId }); +const assertionStack = new Stack(app, 'CertificateIntegAssertions', { env: { account, region: 'us-east-1' } }); +const integ = new IntegTest(app, 'DnsValidatedCertificateV2Integ', { testCases: [owner, consumer], assertionStack }); +integ.assertions + .awsApiCall('ACM', 'describeCertificate', { CertificateArn: certificate.certificateArn }) + .expect(ExpectedResult.objectLike({ Certificate: { Status: 'ISSUED', DomainName: domainName } })); +integ.assertions + .awsApiCall('CloudFront', 'getDistribution', { + Id: Stack.consumeReference(distribution.distributionId, ReferenceStrength.WEAK), + }) + .expect( + ExpectedResult.objectLike({ + Distribution: { + Status: 'Deployed', + DistributionConfig: { ViewerCertificate: { ACMCertificateArn: certificate.certificateArn } }, + }, + }), + ); +app.synth(); diff --git a/tsconfig.dev.json b/tsconfig.dev.json index 48eb4b9..a69d667 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -23,7 +23,12 @@ "strictNullChecks": true, "strictPropertyInitialization": true, "stripInternal": true, - "target": "ES2020" + "target": "ES2020", + "types": [ + "node", + "jest" + ], + "rootDir": "." }, "include": [ "src/**/*.ts",