@aws/nx-plugin 1.0.0-rc.49 → 1.0.0-rc.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE-THIRD-PARTY +436 -29
  2. package/migrations.json +17 -1
  3. package/package.json +2 -1
  4. package/src/migrations/latest/modernize-function-props-cast/migration.d.ts +6 -0
  5. package/src/migrations/latest/modernize-function-props-cast/migration.js +66 -0
  6. package/src/migrations/latest/modernize-function-props-cast/migration.js.map +1 -0
  7. package/src/migrations/latest/restrict-cors-to-custom-domains/migration.d.ts +6 -0
  8. package/src/migrations/latest/restrict-cors-to-custom-domains/migration.js +110 -0
  9. package/src/migrations/latest/restrict-cors-to-custom-domains/migration.js.map +1 -0
  10. package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.d.ts +6 -0
  11. package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.js +129 -0
  12. package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.js.map +1 -0
  13. package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +1 -0
  14. package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +25 -15
  15. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +1 -0
  16. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +26 -16
  17. package/src/terraform/project/files/application/scripts/bootstrap.ts.template +40 -1
  18. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +75 -45
  19. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +1 -0
  20. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +1 -0
  21. package/src/ts/nx-migration/files/migration.ts.template +6 -0
  22. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +1 -0
  23. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +43 -0
  24. package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +11 -14
  25. package/src/utils/__snapshots__/shared-constructs.spec.ts.snap +21 -0
  26. package/src/utils/api-constructs/files/cdk/app/apis/http/__apiNameKebabCase__.ts.template +12 -7
  27. package/src/utils/api-constructs/files/cdk/app/apis/rest/__apiNameKebabCase__.ts.template +13 -8
  28. package/src/utils/files/common/constructs/src/core/cloudfront.ts.template +14 -0
  29. package/src/utils/files/common/constructs/src/core/index.ts.template +1 -0
  30. package/src/utils/format.js +118 -240
  31. package/src/utils/format.js.map +1 -1
  32. package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +7 -14
  33. package/src/utils/ruff.d.ts +59 -0
  34. package/src/utils/ruff.js +140 -0
  35. package/src/utils/ruff.js.map +1 -0
  36. package/src/utils/toml.d.ts +5 -0
  37. package/src/utils/toml.js +10 -0
  38. package/src/utils/toml.js.map +1 -1
  39. package/src/utils/warm-ruff-cache.d.ts +0 -8
  40. package/src/utils/warm-ruff-cache.js +0 -30
  41. package/src/utils/warm-ruff-cache.js.map +0 -1
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { visitNotIgnoredFiles } from "@nx/devkit";
5
+ import { addDestructuredImport, addStarExport, applyGritQL } from "../../../utils/ast.js";
6
+ import { formatFilesInSubtree } from "../../../utils/format.js";
7
+ import { isEsmWorkspace } from "../../../utils/module-format.js";
8
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../../../utils/shared-constructs-constants.js";
9
+ /**
10
+ * Include CloudFront custom domain aliases in restrictCorsTo and UserIdentity callback URLs
11
+ *
12
+ * How to write a migration:
13
+ * - https://nx.dev/docs/kb/migration-generators
14
+ * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject
15
+ *
16
+ * Guardrails:
17
+ * - Pattern-match before writing: skip files that have diverged from the shape
18
+ * your generators produce and report them via `nextSteps`, rather than
19
+ * clobbering the user's changes.
20
+ * - Idempotent: re-running must be a no-op.
21
+ * - Format what you write: finish with `formatFilesInSubtree` so the files your
22
+ * migration wrote are formatted correctly.
23
+ */ const CORE_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core`;
24
+ const APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;
25
+ const CLOUDFRONT_CORE_FILE = `${CORE_DIR}/cloudfront.ts`;
26
+ const CORE_INDEX_FILE = `${CORE_DIR}/index.ts`;
27
+ const USER_IDENTITY_FILE = `${CORE_DIR}/user-identity.ts`;
28
+ const CLOUDFRONT_HELPER_CONTENT = `import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront';
29
+
30
+ /**
31
+ * Finds the domain names associated with a CloudFront distribution.
32
+ *
33
+ * Includes the distribution's default \`*.cloudfront.net\` domain name plus any custom
34
+ * domain names (aliases) configured on it.
35
+ */
36
+ export const findCloudFrontDomainNames = (
37
+ distribution: Distribution,
38
+ ): string[] => {
39
+ const cfnDistribution = distribution.node.defaultChild as CfnDistribution;
40
+ const distributionConfig =
41
+ cfnDistribution.distributionConfig as CfnDistribution.DistributionConfigProperty;
42
+ return [distribution.domainName, ...(distributionConfig.aliases ?? [])];
43
+ };
44
+ `;
45
+ // Rewrites the `restrictCorsTo` body as produced by generators prior to this fix.
46
+ const RESTRICT_CORS_TO_GRITQL_PATTERN = "`origins.map(($o) => typeof $o === 'string' ? $o : 'cloudFrontDistribution' in $o ? $branch1 : $branch2)` where { $branch1 <: contains `distributionDomainName`, $branch2 <: contains `distributionDomainName` } => raw`origins.flatMap(($o) => typeof $o === 'string' ? [$o] : findCloudFrontDomainNames('cloudFrontDistribution' in $o ? $o.cloudFrontDistribution : $o).map((domain) => \\`https://${domain}\\`))`";
47
+ // Rewrites UserIdentity's callback/logout URL logic as produced by generators
48
+ // prior to this fix.
49
+ const USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN = '`this.findCloudFrontDomainNames()` => `Stack.of(this).node.findAll().filter((child): child is Distribution => child instanceof Distribution).flatMap(findCloudFrontDomainNames)`';
50
+ export default async function migration(tree) {
51
+ const nextSteps = [];
52
+ if (!tree.exists(CORE_INDEX_FILE)) {
53
+ // No common/constructs shared library in this workspace - nothing to migrate.
54
+ return {
55
+ nextSteps
56
+ };
57
+ }
58
+ const esm = isEsmWorkspace(tree);
59
+ const apiCloudFrontImportSpecifier = esm ? '../../core/cloudfront.js' : '../../core/cloudfront';
60
+ const coreCloudFrontImportSpecifier = esm ? './cloudfront.js' : './cloudfront';
61
+ if (!tree.exists(CLOUDFRONT_CORE_FILE)) {
62
+ tree.write(CLOUDFRONT_CORE_FILE, CLOUDFRONT_HELPER_CONTENT);
63
+ }
64
+ await addStarExport(tree, CORE_INDEX_FILE, './cloudfront.js');
65
+ const apiAppFiles = [];
66
+ visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath)=>{
67
+ apiAppFiles.push(filePath);
68
+ });
69
+ for (const filePath of apiAppFiles){
70
+ if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {
71
+ continue;
72
+ }
73
+ const rewrote = await applyGritQL(tree, filePath, RESTRICT_CORS_TO_GRITQL_PATTERN);
74
+ if (rewrote) {
75
+ await addDestructuredImport(tree, filePath, [
76
+ 'findCloudFrontDomainNames'
77
+ ], apiCloudFrontImportSpecifier);
78
+ nextSteps.push(`${filePath}: restrictCorsTo now includes CloudFront custom domain aliases automatically.`);
79
+ continue;
80
+ }
81
+ const contents = tree.read(filePath, 'utf-8') ?? '';
82
+ if (!contents.includes('findCloudFrontDomainNames(')) {
83
+ nextSteps.push(`${filePath}: restrictCorsTo has diverged from the generated shape - left untouched. Manually apply the CloudFront custom domain fix (see findCloudFrontDomainNames in common/constructs/src/core/cloudfront.ts).`);
84
+ }
85
+ // Otherwise already migrated - silent skip.
86
+ }
87
+ if (tree.exists(USER_IDENTITY_FILE)) {
88
+ const rewrote = await applyGritQL(tree, USER_IDENTITY_FILE, USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN);
89
+ if (rewrote) {
90
+ await applyGritQL(tree, USER_IDENTITY_FILE, "`import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront'` => `import { Distribution } from 'aws-cdk-lib/aws-cloudfront';`");
91
+ await applyGritQL(tree, USER_IDENTITY_FILE, "or { `// Includes each distribution's default domain name plus any custom domain names (aliases) configured on it.` => ., `private findCloudFrontDomainNames = (): string[] => $body` => . }");
92
+ await addDestructuredImport(tree, USER_IDENTITY_FILE, [
93
+ 'findCloudFrontDomainNames'
94
+ ], coreCloudFrontImportSpecifier);
95
+ nextSteps.push(`${USER_IDENTITY_FILE}: now reuses the shared findCloudFrontDomainNames helper.`);
96
+ } else {
97
+ const contents = tree.read(USER_IDENTITY_FILE, 'utf-8') ?? '';
98
+ if (!contents.includes('.flatMap(findCloudFrontDomainNames)')) {
99
+ nextSteps.push(`${USER_IDENTITY_FILE}: the callback URL logic has diverged from the generated shape - left untouched. Manually apply the shared findCloudFrontDomainNames helper (see common/constructs/src/core/cloudfront.ts).`);
100
+ }
101
+ // Otherwise already migrated - silent skip.
102
+ }
103
+ }
104
+ await formatFilesInSubtree(tree);
105
+ return {
106
+ nextSteps
107
+ };
108
+ }
109
+
110
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/restrict-cors-to-custom-domains/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type MigrationReturnObject,\n type Tree,\n visitNotIgnoredFiles,\n} from '@nx/devkit';\nimport {\n addDestructuredImport,\n addStarExport,\n applyGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport { isEsmWorkspace } from '../../../utils/module-format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Include CloudFront custom domain aliases in restrictCorsTo and UserIdentity callback URLs\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CORE_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core`;\nconst APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;\nconst CLOUDFRONT_CORE_FILE = `${CORE_DIR}/cloudfront.ts`;\nconst CORE_INDEX_FILE = `${CORE_DIR}/index.ts`;\nconst USER_IDENTITY_FILE = `${CORE_DIR}/user-identity.ts`;\n\nconst CLOUDFRONT_HELPER_CONTENT = `import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront';\n\n/**\n * Finds the domain names associated with a CloudFront distribution.\n *\n * Includes the distribution's default \\`*.cloudfront.net\\` domain name plus any custom\n * domain names (aliases) configured on it.\n */\nexport const findCloudFrontDomainNames = (\n distribution: Distribution,\n): string[] => {\n const cfnDistribution = distribution.node.defaultChild as CfnDistribution;\n const distributionConfig =\n cfnDistribution.distributionConfig as CfnDistribution.DistributionConfigProperty;\n return [distribution.domainName, ...(distributionConfig.aliases ?? [])];\n};\n`;\n\n// Rewrites the `restrictCorsTo` body as produced by generators prior to this fix.\nconst RESTRICT_CORS_TO_GRITQL_PATTERN =\n \"`origins.map(($o) => typeof $o === 'string' ? $o : 'cloudFrontDistribution' in $o ? $branch1 : $branch2)` where { $branch1 <: contains `distributionDomainName`, $branch2 <: contains `distributionDomainName` } => raw`origins.flatMap(($o) => typeof $o === 'string' ? [$o] : findCloudFrontDomainNames('cloudFrontDistribution' in $o ? $o.cloudFrontDistribution : $o).map((domain) => \\\\`https://${domain}\\\\`))`\";\n\n// Rewrites UserIdentity's callback/logout URL logic as produced by generators\n// prior to this fix.\nconst USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN =\n '`this.findCloudFrontDomainNames()` => `Stack.of(this).node.findAll().filter((child): child is Distribution => child instanceof Distribution).flatMap(findCloudFrontDomainNames)`';\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(CORE_INDEX_FILE)) {\n // No common/constructs shared library in this workspace - nothing to migrate.\n return { nextSteps };\n }\n\n const esm = isEsmWorkspace(tree);\n const apiCloudFrontImportSpecifier = esm\n ? '../../core/cloudfront.js'\n : '../../core/cloudfront';\n const coreCloudFrontImportSpecifier = esm\n ? './cloudfront.js'\n : './cloudfront';\n\n if (!tree.exists(CLOUDFRONT_CORE_FILE)) {\n tree.write(CLOUDFRONT_CORE_FILE, CLOUDFRONT_HELPER_CONTENT);\n }\n await addStarExport(tree, CORE_INDEX_FILE, './cloudfront.js');\n\n const apiAppFiles: string[] = [];\n visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath) => {\n apiAppFiles.push(filePath);\n });\n\n for (const filePath of apiAppFiles) {\n if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {\n continue;\n }\n const rewrote = await applyGritQL(\n tree,\n filePath,\n RESTRICT_CORS_TO_GRITQL_PATTERN,\n );\n if (rewrote) {\n await addDestructuredImport(\n tree,\n filePath,\n ['findCloudFrontDomainNames'],\n apiCloudFrontImportSpecifier,\n );\n nextSteps.push(\n `${filePath}: restrictCorsTo now includes CloudFront custom domain aliases automatically.`,\n );\n continue;\n }\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (!contents.includes('findCloudFrontDomainNames(')) {\n nextSteps.push(\n `${filePath}: restrictCorsTo has diverged from the generated shape - left untouched. Manually apply the CloudFront custom domain fix (see findCloudFrontDomainNames in common/constructs/src/core/cloudfront.ts).`,\n );\n }\n // Otherwise already migrated - silent skip.\n }\n\n if (tree.exists(USER_IDENTITY_FILE)) {\n const rewrote = await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN,\n );\n if (rewrote) {\n await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n \"`import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront'` => `import { Distribution } from 'aws-cdk-lib/aws-cloudfront';`\",\n );\n await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n \"or { `// Includes each distribution's default domain name plus any custom domain names (aliases) configured on it.` => ., `private findCloudFrontDomainNames = (): string[] => $body` => . }\",\n );\n await addDestructuredImport(\n tree,\n USER_IDENTITY_FILE,\n ['findCloudFrontDomainNames'],\n coreCloudFrontImportSpecifier,\n );\n nextSteps.push(\n `${USER_IDENTITY_FILE}: now reuses the shared findCloudFrontDomainNames helper.`,\n );\n } else {\n const contents = tree.read(USER_IDENTITY_FILE, 'utf-8') ?? '';\n if (!contents.includes('.flatMap(findCloudFrontDomainNames)')) {\n nextSteps.push(\n `${USER_IDENTITY_FILE}: the callback URL logic has diverged from the generated shape - left untouched. Manually apply the shared findCloudFrontDomainNames helper (see common/constructs/src/core/cloudfront.ts).`,\n );\n }\n // Otherwise already migrated - silent skip.\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["visitNotIgnoredFiles","addDestructuredImport","addStarExport","applyGritQL","formatFilesInSubtree","isEsmWorkspace","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","CORE_DIR","APIS_APP_DIR","CLOUDFRONT_CORE_FILE","CORE_INDEX_FILE","USER_IDENTITY_FILE","CLOUDFRONT_HELPER_CONTENT","RESTRICT_CORS_TO_GRITQL_PATTERN","USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN","migration","tree","nextSteps","exists","esm","apiCloudFrontImportSpecifier","coreCloudFrontImportSpecifier","write","apiAppFiles","filePath","push","endsWith","rewrote","contents","read","includes"],"mappings":"AAAA;;;CAGC,GACD,SAGEA,oBAAoB,QACf,aAAa;AACpB,SACEC,qBAAqB,EACrBC,aAAa,EACbC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SAASC,cAAc,QAAQ,kCAA+B;AAC9D,SACEC,YAAY,EACZC,qBAAqB,QAChB,gDAA6C;AAEpD;;;;;;;;;;;;;;CAcC,GAED,MAAMC,WAAW,GAAGF,aAAa,CAAC,EAAEC,sBAAsB,SAAS,CAAC;AACpE,MAAME,eAAe,GAAGH,aAAa,CAAC,EAAEC,sBAAsB,aAAa,CAAC;AAC5E,MAAMG,uBAAuB,GAAGF,SAAS,cAAc,CAAC;AACxD,MAAMG,kBAAkB,GAAGH,SAAS,SAAS,CAAC;AAC9C,MAAMI,qBAAqB,GAAGJ,SAAS,iBAAiB,CAAC;AAEzD,MAAMK,4BAA4B,CAAC;;;;;;;;;;;;;;;;AAgBnC,CAAC;AAED,kFAAkF;AAClF,MAAMC,kCACJ;AAEF,8EAA8E;AAC9E,qBAAqB;AACrB,MAAMC,6CACJ;AAEF,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,IAAI,CAACD,KAAKE,MAAM,CAACR,kBAAkB;QACjC,8EAA8E;QAC9E,OAAO;YAAEO;QAAU;IACrB;IAEA,MAAME,MAAMf,eAAeY;IAC3B,MAAMI,+BAA+BD,MACjC,6BACA;IACJ,MAAME,gCAAgCF,MAClC,oBACA;IAEJ,IAAI,CAACH,KAAKE,MAAM,CAACT,uBAAuB;QACtCO,KAAKM,KAAK,CAACb,sBAAsBG;IACnC;IACA,MAAMX,cAAce,MAAMN,iBAAiB;IAE3C,MAAMa,cAAwB,EAAE;IAChCxB,qBAAqBiB,MAAMR,cAAc,CAACgB;QACxCD,YAAYE,IAAI,CAACD;IACnB;IAEA,KAAK,MAAMA,YAAYD,YAAa;QAClC,IAAI,CAACC,SAASE,QAAQ,CAAC,UAAUF,SAASE,QAAQ,CAAC,cAAc;YAC/D;QACF;QACA,MAAMC,UAAU,MAAMzB,YACpBc,MACAQ,UACAX;QAEF,IAAIc,SAAS;YACX,MAAM3B,sBACJgB,MACAQ,UACA;gBAAC;aAA4B,EAC7BJ;YAEFH,UAAUQ,IAAI,CACZ,GAAGD,SAAS,6EAA6E,CAAC;YAE5F;QACF;QACA,MAAMI,WAAWZ,KAAKa,IAAI,CAACL,UAAU,YAAY;QACjD,IAAI,CAACI,SAASE,QAAQ,CAAC,+BAA+B;YACpDb,UAAUQ,IAAI,CACZ,GAAGD,SAAS,qMAAqM,CAAC;QAEtN;IACA,4CAA4C;IAC9C;IAEA,IAAIR,KAAKE,MAAM,CAACP,qBAAqB;QACnC,MAAMgB,UAAU,MAAMzB,YACpBc,MACAL,oBACAG;QAEF,IAAIa,SAAS;YACX,MAAMzB,YACJc,MACAL,oBACA;YAEF,MAAMT,YACJc,MACAL,oBACA;YAEF,MAAMX,sBACJgB,MACAL,oBACA;gBAAC;aAA4B,EAC7BU;YAEFJ,UAAUQ,IAAI,CACZ,GAAGd,mBAAmB,yDAAyD,CAAC;QAEpF,OAAO;YACL,MAAMiB,WAAWZ,KAAKa,IAAI,CAAClB,oBAAoB,YAAY;YAC3D,IAAI,CAACiB,SAASE,QAAQ,CAAC,wCAAwC;gBAC7Db,UAAUQ,IAAI,CACZ,GAAGd,mBAAmB,2LAA2L,CAAC;YAEtN;QACA,4CAA4C;QAC9C;IACF;IAEA,MAAMR,qBAAqBa;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { type MigrationReturnObject, type Tree } from '@nx/devkit';
6
+ export default function migration(tree: Tree): Promise<MigrationReturnObject>;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { getProjects, joinPathFragments } from "@nx/devkit";
5
+ import { TERRAFORM_PROJECT_GENERATOR_INFO } from "../../../terraform/project/generator.js";
6
+ import { applyGritQL, GRIT_INSERT_PLACEHOLDER, insertViaGritQL, matchGritQL } from "../../../utils/ast.js";
7
+ import { formatFilesInSubtree } from "../../../utils/format.js";
8
+ /**
9
+ * Adopt an existing Terraform state bucket in the vended bootstrap script when its state object is missing
10
+ *
11
+ * The state bucket stores its own tfstate inside itself, so if that object is
12
+ * deleted while the bucket survives, `bootstrap` reads the 404 as a first run
13
+ * and asks terraform to create a bucket that already exists — failing with
14
+ * `BucketAlreadyOwnedByYou` on every subsequent run, with no retry that clears
15
+ * it. The vended script now imports the existing bucket instead.
16
+ *
17
+ * Every edit is expressed as a GritQL rewrite so the script is matched on its
18
+ * AST rather than its formatting.
19
+ *
20
+ * How to write a migration:
21
+ * - https://nx.dev/docs/kb/migration-generators
22
+ * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject
23
+ *
24
+ * Guardrails:
25
+ * - Pattern-match before writing: skip files that have diverged from the shape
26
+ * your generators produce and report them via `nextSteps`, or consider a
27
+ * hybrid migration, rather than clobbering the user's changes.
28
+ * - Idempotent: re-running must be a no-op.
29
+ * - Format what you write: finish with `formatFilesInSubtree` so the files your
30
+ * migration wrote are formatted correctly.
31
+ */ // Guards the whole migration: present only once the import step exists.
32
+ const MIGRATED_PATTERN = '`bucketExists($_, $_)`';
33
+ // Every edit site, matched structurally so formatting and argument layout
34
+ // don't affect whether the script is recognised.
35
+ const STATE_FETCH_TRY_PATTERN = 'try_statement() as $try where { $try <: contains `GetObjectCommand` }';
36
+ const STATE_FLAG_SET_PATTERN = '`if (out.Body) { $body }`';
37
+ const HELPER_PATTERN = '`const main = async () => { $body }`';
38
+ const INIT_PATTERN = "`execFileSync('terraform', ['init'], $opts)`";
39
+ const S3_IMPORT_PATTERN = "`import { $before, GetObjectCommand, $after } from '@aws-sdk/client-s3'`";
40
+ const HEADER_COMMENT_PATTERN = 'comment() as $c where { $c <: includes "state back to S3." }';
41
+ // GritQL snippets are parsed as backtick-quoted patterns, so any backtick in
42
+ // the inserted source has to survive that layer escaped.
43
+ const BUCKET_EXISTS_HELPER = `// S3 answers 404 only when the name is genuinely free; 403 means it exists
44
+ // but is not readable with these credentials.
45
+ const bucketExists = async (s3: S3Client, bucket: string) => {
46
+ try {
47
+ await s3.send(new HeadBucketCommand({ Bucket: bucket }));
48
+ return true;
49
+ } catch (err: any) {
50
+ const status = err?.$metadata?.httpStatusCode;
51
+ if (status === 404) return false;
52
+ if (status === 403) return true;
53
+ throw err;
54
+ }
55
+ };`;
56
+ const IMPORT_STEP = `// The bucket holds its own state, so losing the state object while the
57
+ // bucket survives would otherwise wedge bootstrap on a permanent
58
+ // \`BucketAlreadyOwnedByYou\`. Adopt the existing bucket instead.
59
+ if (!haveState && (await bucketExists(s3, bucket))) {
60
+ console.log(
61
+ \`State bucket \${bucket} already exists but its bootstrap state is missing — importing it.\`,
62
+ );
63
+ execFileSync(
64
+ 'terraform',
65
+ [
66
+ 'import',
67
+ \`-state=\${tfStatePath}\`,
68
+ \`-var=aws_region=\${region}\`,
69
+ 'aws_s3_bucket.terraform_state',
70
+ bucket,
71
+ ],
72
+ { cwd: bootstrapDir, stdio: 'inherit' },
73
+ );
74
+ }`;
75
+ // Rewritten wholesale rather than appended to, since the summary sentence
76
+ // changes rather than gaining a clause.
77
+ const NEW_HEADER_COMMENT = `/**
78
+ * Bootstraps the remote Terraform state bucket.
79
+ *
80
+ * Equivalent to \\\`cdk bootstrap\\\` — resolves account + region from the AWS
81
+ * SDK credential chain, pulls any existing bootstrap tfstate from S3,
82
+ * runs \\\`terraform apply\\\` in the \\\`bootstrap\\\` dir, then pushes the new
83
+ * state back to S3. Adopts an already-existing state bucket when its
84
+ * state object is missing.
85
+ */`;
86
+ const DIVERGED_NEXT_STEP = (filePath)=>`${filePath}: the bootstrap script has diverged from the generated shape - left untouched. Manually \`terraform import aws_s3_bucket.terraform_state <bucket>\` before \`terraform apply\` when the state bucket exists but its \`bootstrap.tfstate\` object does not, otherwise bootstrap fails with BucketAlreadyOwnedByYou.`;
87
+ export default async function migration(tree) {
88
+ const nextSteps = [];
89
+ // Terraform application projects are the only ones that vend bootstrap.ts.
90
+ const bootstrapScripts = [
91
+ ...getProjects(tree).values()
92
+ ].filter((project)=>project.metadata?.generator === TERRAFORM_PROJECT_GENERATOR_INFO.id).map((project)=>joinPathFragments(project.root, 'scripts/bootstrap.ts')).filter((filePath)=>tree.exists(filePath));
93
+ for (const filePath of bootstrapScripts){
94
+ if (await matchGritQL(tree, filePath, MIGRATED_PATTERN)) {
95
+ continue;
96
+ }
97
+ // Confirm every edit site is present before writing any of them, so a
98
+ // script that only partly matches is left whole rather than half-edited.
99
+ const allSitesPresent = (await Promise.all([
100
+ STATE_FETCH_TRY_PATTERN,
101
+ STATE_FLAG_SET_PATTERN,
102
+ HELPER_PATTERN,
103
+ INIT_PATTERN,
104
+ S3_IMPORT_PATTERN,
105
+ HEADER_COMMENT_PATTERN
106
+ ].map((pattern)=>matchGritQL(tree, filePath, pattern)))).every(Boolean);
107
+ if (!allSitesPresent) {
108
+ nextSteps.push(DIVERGED_NEXT_STEP(filePath));
109
+ continue;
110
+ }
111
+ // `haveState` distinguishes "no remote state" from "no bucket", so the
112
+ // import only runs when terraform has no prior knowledge of the bucket.
113
+ await insertViaGritQL(tree, filePath, `${STATE_FETCH_TRY_PATTERN} => \`${GRIT_INSERT_PLACEHOLDER}\n $try\``, 'let haveState = false;');
114
+ await applyGritQL(tree, filePath, `${STATE_FLAG_SET_PATTERN} => \`if (out.Body) {\n $body\n haveState = true;\n }\``);
115
+ await insertViaGritQL(tree, filePath, `${HELPER_PATTERN} => \`${GRIT_INSERT_PLACEHOLDER}\n\nconst main = async () => { $body }\``, BUCKET_EXISTS_HELPER);
116
+ await insertViaGritQL(tree, filePath, `${INIT_PATTERN} => \`execFileSync('terraform', ['init'], $opts);\n\n ${GRIT_INSERT_PLACEHOLDER}\n\``, IMPORT_STEP);
117
+ // Placed in the generator's import position rather than appended, so a
118
+ // migrated workspace matches a freshly generated one.
119
+ await applyGritQL(tree, filePath, `${S3_IMPORT_PATTERN} => \`import { $before, GetObjectCommand, HeadBucketCommand, $after } from '@aws-sdk/client-s3'\``);
120
+ await applyGritQL(tree, filePath, `${HEADER_COMMENT_PATTERN} => \`${NEW_HEADER_COMMENT}\``);
121
+ nextSteps.push(`${filePath}: now adopts an existing Terraform state bucket when its bootstrap.tfstate object is missing.`);
122
+ }
123
+ await formatFilesInSubtree(tree);
124
+ return {
125
+ nextSteps
126
+ };
127
+ }
128
+
129
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type Tree,\n} from '@nx/devkit';\nimport { TERRAFORM_PROJECT_GENERATOR_INFO } from '../../../terraform/project/generator';\nimport {\n applyGritQL,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\n\n/**\n * Adopt an existing Terraform state bucket in the vended bootstrap script when its state object is missing\n *\n * The state bucket stores its own tfstate inside itself, so if that object is\n * deleted while the bucket survives, `bootstrap` reads the 404 as a first run\n * and asks terraform to create a bucket that already exists — failing with\n * `BucketAlreadyOwnedByYou` on every subsequent run, with no retry that clears\n * it. The vended script now imports the existing bucket instead.\n *\n * Every edit is expressed as a GritQL rewrite so the script is matched on its\n * AST rather than its formatting.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, or consider a\n * hybrid migration, rather than clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\n// Guards the whole migration: present only once the import step exists.\nconst MIGRATED_PATTERN = '`bucketExists($_, $_)`';\n\n// Every edit site, matched structurally so formatting and argument layout\n// don't affect whether the script is recognised.\nconst STATE_FETCH_TRY_PATTERN =\n 'try_statement() as $try where { $try <: contains `GetObjectCommand` }';\nconst STATE_FLAG_SET_PATTERN = '`if (out.Body) { $body }`';\nconst HELPER_PATTERN = '`const main = async () => { $body }`';\nconst INIT_PATTERN = \"`execFileSync('terraform', ['init'], $opts)`\";\nconst S3_IMPORT_PATTERN =\n \"`import { $before, GetObjectCommand, $after } from '@aws-sdk/client-s3'`\";\nconst HEADER_COMMENT_PATTERN =\n 'comment() as $c where { $c <: includes \"state back to S3.\" }';\n\n// GritQL snippets are parsed as backtick-quoted patterns, so any backtick in\n// the inserted source has to survive that layer escaped.\nconst BUCKET_EXISTS_HELPER = `// S3 answers 404 only when the name is genuinely free; 403 means it exists\n// but is not readable with these credentials.\nconst bucketExists = async (s3: S3Client, bucket: string) => {\n try {\n await s3.send(new HeadBucketCommand({ Bucket: bucket }));\n return true;\n } catch (err: any) {\n const status = err?.$metadata?.httpStatusCode;\n if (status === 404) return false;\n if (status === 403) return true;\n throw err;\n }\n};`;\n\nconst IMPORT_STEP = `// The bucket holds its own state, so losing the state object while the\n // bucket survives would otherwise wedge bootstrap on a permanent\n // \\`BucketAlreadyOwnedByYou\\`. Adopt the existing bucket instead.\n if (!haveState && (await bucketExists(s3, bucket))) {\n console.log(\n \\`State bucket \\${bucket} already exists but its bootstrap state is missing — importing it.\\`,\n );\n execFileSync(\n 'terraform',\n [\n 'import',\n \\`-state=\\${tfStatePath}\\`,\n \\`-var=aws_region=\\${region}\\`,\n 'aws_s3_bucket.terraform_state',\n bucket,\n ],\n { cwd: bootstrapDir, stdio: 'inherit' },\n );\n }`;\n\n// Rewritten wholesale rather than appended to, since the summary sentence\n// changes rather than gaining a clause.\nconst NEW_HEADER_COMMENT = `/**\n * Bootstraps the remote Terraform state bucket.\n *\n * Equivalent to \\\\\\`cdk bootstrap\\\\\\` — resolves account + region from the AWS\n * SDK credential chain, pulls any existing bootstrap tfstate from S3,\n * runs \\\\\\`terraform apply\\\\\\` in the \\\\\\`bootstrap\\\\\\` dir, then pushes the new\n * state back to S3. Adopts an already-existing state bucket when its\n * state object is missing.\n */`;\n\nconst DIVERGED_NEXT_STEP = (filePath: string) =>\n `${filePath}: the bootstrap script has diverged from the generated shape - left untouched. Manually \\`terraform import aws_s3_bucket.terraform_state <bucket>\\` before \\`terraform apply\\` when the state bucket exists but its \\`bootstrap.tfstate\\` object does not, otherwise bootstrap fails with BucketAlreadyOwnedByYou.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n // Terraform application projects are the only ones that vend bootstrap.ts.\n const bootstrapScripts = [...getProjects(tree).values()]\n .filter(\n (project) =>\n (project.metadata as any)?.generator ===\n TERRAFORM_PROJECT_GENERATOR_INFO.id,\n )\n .map((project) => joinPathFragments(project.root, 'scripts/bootstrap.ts'))\n .filter((filePath) => tree.exists(filePath));\n\n for (const filePath of bootstrapScripts) {\n if (await matchGritQL(tree, filePath, MIGRATED_PATTERN)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n continue;\n }\n\n // Confirm every edit site is present before writing any of them, so a\n // script that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n [\n STATE_FETCH_TRY_PATTERN,\n STATE_FLAG_SET_PATTERN,\n HELPER_PATTERN,\n INIT_PATTERN,\n S3_IMPORT_PATTERN,\n HEADER_COMMENT_PATTERN,\n ].map((pattern) => matchGritQL(tree, filePath, pattern)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(DIVERGED_NEXT_STEP(filePath));\n continue;\n }\n\n // `haveState` distinguishes \"no remote state\" from \"no bucket\", so the\n // import only runs when terraform has no prior knowledge of the bucket.\n await insertViaGritQL(\n tree,\n filePath,\n `${STATE_FETCH_TRY_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n $try\\``,\n 'let haveState = false;',\n );\n await applyGritQL(\n tree,\n filePath,\n `${STATE_FLAG_SET_PATTERN} => \\`if (out.Body) {\\n $body\\n haveState = true;\\n }\\``,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${HELPER_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n\\nconst main = async () => { $body }\\``,\n BUCKET_EXISTS_HELPER,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${INIT_PATTERN} => \\`execFileSync('terraform', ['init'], $opts);\\n\\n ${GRIT_INSERT_PLACEHOLDER}\\n\\``,\n IMPORT_STEP,\n );\n\n // Placed in the generator's import position rather than appended, so a\n // migrated workspace matches a freshly generated one.\n await applyGritQL(\n tree,\n filePath,\n `${S3_IMPORT_PATTERN} => \\`import { $before, GetObjectCommand, HeadBucketCommand, $after } from '@aws-sdk/client-s3'\\``,\n );\n\n await applyGritQL(\n tree,\n filePath,\n `${HEADER_COMMENT_PATTERN} => \\`${NEW_HEADER_COMMENT}\\``,\n );\n\n nextSteps.push(\n `${filePath}: now adopts an existing Terraform state bucket when its bootstrap.tfstate object is missing.`,\n );\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","TERRAFORM_PROJECT_GENERATOR_INFO","applyGritQL","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","MIGRATED_PATTERN","STATE_FETCH_TRY_PATTERN","STATE_FLAG_SET_PATTERN","HELPER_PATTERN","INIT_PATTERN","S3_IMPORT_PATTERN","HEADER_COMMENT_PATTERN","BUCKET_EXISTS_HELPER","IMPORT_STEP","NEW_HEADER_COMMENT","DIVERGED_NEXT_STEP","filePath","migration","tree","nextSteps","bootstrapScripts","values","filter","project","metadata","generator","id","map","root","exists","allSitesPresent","Promise","all","pattern","every","Boolean","push"],"mappings":"AAAA;;;CAGC,GACD,SACEA,WAAW,EACXC,iBAAiB,QAGZ,aAAa;AACpB,SAASC,gCAAgC,QAAQ,0CAAuC;AACxF,SACEC,WAAW,EACXC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAE7D;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,wEAAwE;AACxE,MAAMC,mBAAmB;AAEzB,0EAA0E;AAC1E,iDAAiD;AACjD,MAAMC,0BACJ;AACF,MAAMC,yBAAyB;AAC/B,MAAMC,iBAAiB;AACvB,MAAMC,eAAe;AACrB,MAAMC,oBACJ;AACF,MAAMC,yBACJ;AAEF,6EAA6E;AAC7E,yDAAyD;AACzD,MAAMC,uBAAuB,CAAC;;;;;;;;;;;;EAY5B,CAAC;AAEH,MAAMC,cAAc,CAAC;;;;;;;;;;;;;;;;;;GAkBlB,CAAC;AAEJ,0EAA0E;AAC1E,wCAAwC;AACxC,MAAMC,qBAAqB,CAAC;;;;;;;;GAQzB,CAAC;AAEJ,MAAMC,qBAAqB,CAACC,WAC1B,GAAGA,SAAS,kTAAkT,CAAC;AAEjU,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,2EAA2E;IAC3E,MAAMC,mBAAmB;WAAIvB,YAAYqB,MAAMG,MAAM;KAAG,CACrDC,MAAM,CACL,CAACC,UACC,AAACA,QAAQC,QAAQ,EAAUC,cAC3B1B,iCAAiC2B,EAAE,EAEtCC,GAAG,CAAC,CAACJ,UAAYzB,kBAAkByB,QAAQK,IAAI,EAAE,yBACjDN,MAAM,CAAC,CAACN,WAAaE,KAAKW,MAAM,CAACb;IAEpC,KAAK,MAAMA,YAAYI,iBAAkB;QACvC,IAAI,MAAMjB,YAAYe,MAAMF,UAAUX,mBAAmB;YAEvD;QACF;QAEA,sEAAsE;QACtE,yEAAyE;QACzE,MAAMyB,kBAAkB,AACtB,CAAA,MAAMC,QAAQC,GAAG,CACf;YACE1B;YACAC;YACAC;YACAC;YACAC;YACAC;SACD,CAACgB,GAAG,CAAC,CAACM,UAAY9B,YAAYe,MAAMF,UAAUiB,UACjD,EACAC,KAAK,CAACC;QAER,IAAI,CAACL,iBAAiB;YACpBX,UAAUiB,IAAI,CAACrB,mBAAmBC;YAClC;QACF;QAEA,uEAAuE;QACvE,wEAAwE;QACxE,MAAMd,gBACJgB,MACAF,UACA,GAAGV,wBAAwB,MAAM,EAAEL,wBAAwB,UAAU,CAAC,EACtE;QAEF,MAAMD,YACJkB,MACAF,UACA,GAAGT,uBAAuB,oEAAoE,CAAC;QAEjG,MAAML,gBACJgB,MACAF,UACA,GAAGR,eAAe,MAAM,EAAEP,wBAAwB,wCAAwC,CAAC,EAC3FW;QAEF,MAAMV,gBACJgB,MACAF,UACA,GAAGP,aAAa,uDAAuD,EAAER,wBAAwB,IAAI,CAAC,EACtGY;QAGF,uEAAuE;QACvE,sDAAsD;QACtD,MAAMb,YACJkB,MACAF,UACA,GAAGN,kBAAkB,iGAAiG,CAAC;QAGzH,MAAMV,YACJkB,MACAF,UACA,GAAGL,uBAAuB,MAAM,EAAEG,mBAAmB,EAAE,CAAC;QAG1DK,UAAUiB,IAAI,CACZ,GAAGpB,SAAS,6FAA6F,CAAC;IAE9G;IAEA,MAAMZ,qBAAqBc;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
@@ -171,6 +171,7 @@ exports[`py#agent generator > should match snapshot for BedrockAgentCoreRuntime
171
171
  exports[`py#agent generator > should match snapshot for BedrockAgentCoreRuntime generated constructs files > core-index.ts 1`] = `
172
172
  "export * from './app.js';
173
173
  export * from './checkov.js';
174
+ export * from './cloudfront.js';
174
175
  export * from './runtime-config.js';
175
176
  export * from './workspace.js';
176
177
  "
@@ -434,6 +434,7 @@ import {
434
434
  HttpApiIntegration,
435
435
  IntegrationBuilder,
436
436
  } from '../../core/api/utils.js';
437
+ import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
437
438
  import { HttpApi } from '../../core/api/http-api.js';
438
439
  import {
439
440
  OPERATION_DETAILS,
@@ -474,7 +475,7 @@ export class TestApi<
474
475
  return IntegrationBuilder.http({
475
476
  pattern: 'isolated',
476
477
  operations: OPERATION_DETAILS,
477
- defaultIntegrationOptions: <FunctionProps>{
478
+ defaultIntegrationOptions: {
478
479
  runtime: Runtime.PYTHON_3_14,
479
480
  handler: 'run.sh',
480
481
  code: Code.fromAsset(
@@ -488,7 +489,7 @@ export class TestApi<
488
489
  timeout: Duration.seconds(30),
489
490
  tracing: Tracing.ACTIVE,
490
491
  snapStart: SnapStartConf.ON_PUBLISHED_VERSIONS,
491
- },
492
+ } as FunctionProps,
492
493
  buildDefaultIntegration: (op, props: FunctionProps) => {
493
494
  const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
494
495
  handler.addEnvironment(
@@ -549,6 +550,8 @@ export class TestApi<
549
550
  * as the only permitted CORS origins
550
551
  * in the API gateway. The CORS origins are not configured within the AWS Lambda
551
552
  * integrations since the associated header is controlled by API Gateway v2.
553
+ * Any custom domain names (aliases) configured on a CloudFront distribution are
554
+ * included automatically alongside its default \`*.cloudfront.net\` domain.
552
555
  *
553
556
  * @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
554
557
  */
@@ -559,12 +562,14 @@ export class TestApi<
559
562
  | { cloudFrontDistribution: Distribution }
560
563
  )[]
561
564
  ) {
562
- const allowedOrigins = origins.map((origin) =>
565
+ const allowedOrigins = origins.flatMap((origin) =>
563
566
  typeof origin === 'string'
564
- ? origin
565
- : 'cloudFrontDistribution' in origin
566
- ? \`https://\${origin.cloudFrontDistribution.distributionDomainName}\`
567
- : \`https://\${origin.distributionDomainName}\`,
567
+ ? [origin]
568
+ : findCloudFrontDomainNames(
569
+ 'cloudFrontDistribution' in origin
570
+ ? origin.cloudFrontDistribution
571
+ : origin,
572
+ ).map((domain) => \`https://\${domain}\`),
568
573
  );
569
574
 
570
575
  const cfnApi = this.api.node.defaultChild;
@@ -1406,6 +1411,7 @@ import {
1406
1411
  IntegrationBuilder,
1407
1412
  RestApiIntegration,
1408
1413
  } from '../../core/api/utils.js';
1414
+ import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
1409
1415
  import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';
1410
1416
  import {
1411
1417
  OPERATION_DETAILS,
@@ -1454,7 +1460,7 @@ export class TestApi<
1454
1460
  return IntegrationBuilder.rest({
1455
1461
  pattern: 'isolated',
1456
1462
  operations: OPERATION_DETAILS,
1457
- defaultIntegrationOptions: <FunctionProps>{
1463
+ defaultIntegrationOptions: {
1458
1464
  runtime: Runtime.PYTHON_3_14,
1459
1465
  handler: 'run.sh',
1460
1466
  code: Code.fromAsset(
@@ -1468,7 +1474,7 @@ export class TestApi<
1468
1474
  timeout: Duration.seconds(30),
1469
1475
  tracing: Tracing.ACTIVE,
1470
1476
  snapStart: SnapStartConf.ON_PUBLISHED_VERSIONS,
1471
- },
1477
+ } as FunctionProps,
1472
1478
  buildDefaultIntegration: (op, props: FunctionProps) => {
1473
1479
  const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
1474
1480
  handler.addEnvironment(
@@ -1532,7 +1538,9 @@ export class TestApi<
1532
1538
  *
1533
1539
  * Configures the provided CloudFront distribution domains or origin strings
1534
1540
  * as the only permitted CORS origins in API Gateway preflight responses and the
1535
- * AWS Lambda integrations.
1541
+ * AWS Lambda integrations. Any custom domain names (aliases) configured on a
1542
+ * CloudFront distribution are included automatically alongside its default
1543
+ * \`*.cloudfront.net\` domain.
1536
1544
  *
1537
1545
  * @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
1538
1546
  */
@@ -1543,12 +1551,14 @@ export class TestApi<
1543
1551
  | { cloudFrontDistribution: Distribution }
1544
1552
  )[]
1545
1553
  ) {
1546
- const allowedOrigins = origins.map((origin) =>
1554
+ const allowedOrigins = origins.flatMap((origin) =>
1547
1555
  typeof origin === 'string'
1548
- ? origin
1549
- : 'cloudFrontDistribution' in origin
1550
- ? \`https://\${origin.cloudFrontDistribution.distributionDomainName}\`
1551
- : \`https://\${origin.distributionDomainName}\`,
1556
+ ? [origin]
1557
+ : findCloudFrontDomainNames(
1558
+ 'cloudFrontDistribution' in origin
1559
+ ? origin.cloudFrontDistribution
1560
+ : origin,
1561
+ ).map((domain) => \`https://\${domain}\`),
1552
1562
  );
1553
1563
 
1554
1564
  this.allowedOrigins = allowedOrigins;
@@ -8,6 +8,7 @@ exports[`py#mcp-server generator > should match snapshot for BedrockAgentCoreRun
8
8
  exports[`py#mcp-server generator > should match snapshot for BedrockAgentCoreRuntime generated constructs files > core-index.ts 1`] = `
9
9
  "export * from './app.js';
10
10
  export * from './checkov.js';
11
+ export * from './cloudfront.js';
11
12
  export * from './runtime-config.js';
12
13
  export * from './workspace.js';
13
14
  "
@@ -32,6 +32,7 @@ import {
32
32
  IntegrationBuilder,
33
33
  RestApiIntegration,
34
34
  } from '../../core/api/utils.js';
35
+ import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
35
36
  import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';
36
37
  import {
37
38
  OPERATION_DETAILS,
@@ -86,7 +87,7 @@ export class TestApi<
86
87
  return IntegrationBuilder.rest({
87
88
  pattern: 'isolated',
88
89
  operations: OPERATION_DETAILS,
89
- defaultIntegrationOptions: <FunctionProps>{
90
+ defaultIntegrationOptions: {
90
91
  runtime: Runtime.NODEJS_LATEST,
91
92
  handler: 'index.handler',
92
93
  code: Code.fromAsset(
@@ -99,7 +100,7 @@ export class TestApi<
99
100
  ),
100
101
  timeout: Duration.seconds(30),
101
102
  tracing: Tracing.ACTIVE,
102
- },
103
+ } as FunctionProps,
103
104
  buildDefaultIntegration: (op, props: FunctionProps) => {
104
105
  const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
105
106
  handler.addEnvironment(
@@ -156,7 +157,9 @@ export class TestApi<
156
157
  *
157
158
  * Configures the provided CloudFront distribution domains or origin strings
158
159
  * as the only permitted CORS origins in API Gateway preflight responses and the
159
- * AWS Lambda integrations.
160
+ * AWS Lambda integrations. Any custom domain names (aliases) configured on a
161
+ * CloudFront distribution are included automatically alongside its default
162
+ * \`*.cloudfront.net\` domain.
160
163
  *
161
164
  * @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
162
165
  */
@@ -167,12 +170,14 @@ export class TestApi<
167
170
  | { cloudFrontDistribution: Distribution }
168
171
  )[]
169
172
  ) {
170
- const allowedOrigins = origins.map((origin) =>
173
+ const allowedOrigins = origins.flatMap((origin) =>
171
174
  typeof origin === 'string'
172
- ? origin
173
- : 'cloudFrontDistribution' in origin
174
- ? \`https://\${origin.cloudFrontDistribution.distributionDomainName}\`
175
- : \`https://\${origin.distributionDomainName}\`,
175
+ ? [origin]
176
+ : findCloudFrontDomainNames(
177
+ 'cloudFrontDistribution' in origin
178
+ ? origin.cloudFrontDistribution
179
+ : origin,
180
+ ).map((domain) => \`https://\${domain}\`),
176
181
  );
177
182
 
178
183
  this.allowedOrigins = allowedOrigins;
@@ -221,6 +226,7 @@ import {
221
226
  IntegrationBuilder,
222
227
  RestApiIntegration,
223
228
  } from '../../core/api/utils.js';
229
+ import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
224
230
  import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';
225
231
  import {
226
232
  OPERATION_DETAILS,
@@ -269,7 +275,7 @@ export class TestApi<
269
275
  return IntegrationBuilder.rest({
270
276
  pattern: 'isolated',
271
277
  operations: OPERATION_DETAILS,
272
- defaultIntegrationOptions: <FunctionProps>{
278
+ defaultIntegrationOptions: {
273
279
  runtime: Runtime.NODEJS_LATEST,
274
280
  handler: 'index.handler',
275
281
  code: Code.fromAsset(
@@ -282,7 +288,7 @@ export class TestApi<
282
288
  ),
283
289
  timeout: Duration.seconds(30),
284
290
  tracing: Tracing.ACTIVE,
285
- },
291
+ } as FunctionProps,
286
292
  buildDefaultIntegration: (op, props: FunctionProps) => {
287
293
  const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
288
294
  handler.addEnvironment(
@@ -333,7 +339,9 @@ export class TestApi<
333
339
  *
334
340
  * Configures the provided CloudFront distribution domains or origin strings
335
341
  * as the only permitted CORS origins in API Gateway preflight responses and the
336
- * AWS Lambda integrations.
342
+ * AWS Lambda integrations. Any custom domain names (aliases) configured on a
343
+ * CloudFront distribution are included automatically alongside its default
344
+ * \`*.cloudfront.net\` domain.
337
345
  *
338
346
  * @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
339
347
  */
@@ -344,12 +352,14 @@ export class TestApi<
344
352
  | { cloudFrontDistribution: Distribution }
345
353
  )[]
346
354
  ) {
347
- const allowedOrigins = origins.map((origin) =>
355
+ const allowedOrigins = origins.flatMap((origin) =>
348
356
  typeof origin === 'string'
349
- ? origin
350
- : 'cloudFrontDistribution' in origin
351
- ? \`https://\${origin.cloudFrontDistribution.distributionDomainName}\`
352
- : \`https://\${origin.distributionDomainName}\`,
357
+ ? [origin]
358
+ : findCloudFrontDomainNames(
359
+ 'cloudFrontDistribution' in origin
360
+ ? origin.cloudFrontDistribution
361
+ : origin,
362
+ ).map((domain) => \`https://\${domain}\`),
353
363
  );
354
364
 
355
365
  this.allowedOrigins = allowedOrigins;
@@ -4,7 +4,8 @@
4
4
  * Equivalent to `cdk bootstrap` — resolves account + region from the AWS
5
5
  * SDK credential chain, pulls any existing bootstrap tfstate from S3,
6
6
  * runs `terraform apply` in the `bootstrap` dir, then pushes the new
7
- * state back to S3.
7
+ * state back to S3. Adopts an already-existing state bucket when its
8
+ * state object is missing.
8
9
  */
9
10
  import { execFileSync } from 'node:child_process';
10
11
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -12,6 +13,7 @@ import { dirname, join, resolve } from 'node:path';
12
13
  import {
13
14
  S3Client,
14
15
  GetObjectCommand,
16
+ HeadBucketCommand,
15
17
  PutObjectCommand,
16
18
  } from '@aws-sdk/client-s3';
17
19
  import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
@@ -34,6 +36,20 @@ const tfStatePath = join(
34
36
  'bootstrap.tfstate',
35
37
  );
36
38
 
39
+ // S3 answers 404 only when the name is genuinely free; 403 means it exists
40
+ // but is not readable with these credentials.
41
+ const bucketExists = async (s3: S3Client, bucket: string) => {
42
+ try {
43
+ await s3.send(new HeadBucketCommand({ Bucket: bucket }));
44
+ return true;
45
+ } catch (err: any) {
46
+ const status = err?.$metadata?.httpStatusCode;
47
+ if (status === 404) return false;
48
+ if (status === 403) return true;
49
+ throw err;
50
+ }
51
+ };
52
+
37
53
  const main = async () => {
38
54
  const { accountId, region } = await resolveAwsConfig();
39
55
  const bucket = `${accountId}-tf-state-${region}`;
@@ -46,12 +62,14 @@ const main = async () => {
46
62
  // Pull any existing bootstrap tfstate. First-time bootstrap has no
47
63
  // remote state yet — fall through and let terraform apply create the
48
64
  // bucket.
65
+ let haveState = false;
49
66
  try {
50
67
  const out = await s3.send(
51
68
  new GetObjectCommand({ Bucket: bucket, Key: key }),
52
69
  );
53
70
  if (out.Body) {
54
71
  writeFileSync(tfStatePath, await out.Body.transformToByteArray());
72
+ haveState = true;
55
73
  }
56
74
  } catch (err: any) {
57
75
  const name = err?.name ?? '';
@@ -65,6 +83,27 @@ const main = async () => {
65
83
  }
66
84
 
67
85
  execFileSync('terraform', ['init'], { cwd: bootstrapDir, stdio: 'inherit' });
86
+
87
+ // The bucket holds its own state, so losing the state object while the
88
+ // bucket survives would otherwise wedge bootstrap on a permanent
89
+ // `BucketAlreadyOwnedByYou`. Adopt the existing bucket instead.
90
+ if (!haveState && (await bucketExists(s3, bucket))) {
91
+ console.log(
92
+ `State bucket ${bucket} already exists but its bootstrap state is missing — importing it.`,
93
+ );
94
+ execFileSync(
95
+ 'terraform',
96
+ [
97
+ 'import',
98
+ `-state=${tfStatePath}`,
99
+ `-var=aws_region=${region}`,
100
+ 'aws_s3_bucket.terraform_state',
101
+ bucket,
102
+ ],
103
+ { cwd: bootstrapDir, stdio: 'inherit' },
104
+ );
105
+ }
106
+
68
107
  execFileSync(
69
108
  'terraform',
70
109
  [