@fjall/components-infrastructure 17.0.0 → 18.0.0
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.
- package/dist/lib/config/aws/scpPreset.js +103 -17
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +1 -0
- package/dist/lib/patterns/aws/storage.d.ts +8 -0
- package/dist/lib/patterns/aws/storage.js +10 -3
- package/dist/lib/patterns/aws/targets/fjallTargets.d.ts +7 -5
- package/dist/lib/patterns/aws/targets/fjallTargets.js +9 -6
- package/dist/lib/patterns/aws/targets/targetResolution.d.ts +8 -5
- package/dist/lib/patterns/aws/targets/targetResolution.js +11 -8
- package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +10 -1
- package/dist/lib/resources/aws/storage/s3.d.ts +19 -7
- package/dist/lib/resources/aws/storage/s3.js +15 -3
- package/package.json +3 -3
|
@@ -15,6 +15,108 @@ const EXEMPT_ROLE_PATTERNS = [
|
|
|
15
15
|
const SCP_BYTE_LIMIT = 5120;
|
|
16
16
|
const IAM_POLICY_VERSION = "2012-10-17";
|
|
17
17
|
const SCP_POLICY_TYPE = "SERVICE_CONTROL_POLICY";
|
|
18
|
+
// Region-deny exemptions: global and us-east-1-locked services evaluate
|
|
19
|
+
// aws:RequestedRegion as us-east-1, so a region deny without these blocks
|
|
20
|
+
// their consoles outright for orgs whose allowedRegions exclude us-east-1.
|
|
21
|
+
// Mirrors the NotAction list of AWS Control Tower's Region deny control
|
|
22
|
+
// (GRREGIONDENY) so future re-syncs are a mechanical diff:
|
|
23
|
+
// https://docs.aws.amazon.com/controltower/latest/controlreference/primary-region-deny-policy.html
|
|
24
|
+
// Deliberate deltas from that list:
|
|
25
|
+
// - "support-console:*" — Support Center console auth moves to this prefix
|
|
26
|
+
// on 2026-11-16 (support:* remains the API prefix); predates the CT list.
|
|
27
|
+
// - "purchase-orders:*" — the one aws-portal-successor billing prefix the CT
|
|
28
|
+
// list is missing.
|
|
29
|
+
// - "tag:*" — grandfathered superset of CT's "tag:GetResources"; narrowing
|
|
30
|
+
// a deployed deny would be a breaking tightening.
|
|
31
|
+
const GLOBAL_SERVICE_EXEMPTIONS = [
|
|
32
|
+
"a4b:*",
|
|
33
|
+
"access-analyzer:*",
|
|
34
|
+
"account:*",
|
|
35
|
+
"acm:*",
|
|
36
|
+
"activate:*",
|
|
37
|
+
"artifact:*",
|
|
38
|
+
"aws-marketplace-management:*",
|
|
39
|
+
"aws-marketplace:*",
|
|
40
|
+
"aws-portal:*",
|
|
41
|
+
"billing:*",
|
|
42
|
+
"billingconductor:*",
|
|
43
|
+
"budgets:*",
|
|
44
|
+
"ce:*",
|
|
45
|
+
"chatbot:*",
|
|
46
|
+
"chime:*",
|
|
47
|
+
"cloudfront:*",
|
|
48
|
+
"cloudtrail:LookupEvents",
|
|
49
|
+
"compute-optimizer:*",
|
|
50
|
+
"config:*",
|
|
51
|
+
"consoleapp:*",
|
|
52
|
+
"consolidatedbilling:*",
|
|
53
|
+
"cur:*",
|
|
54
|
+
"datapipeline:GetAccountLimits",
|
|
55
|
+
"devicefarm:*",
|
|
56
|
+
"directconnect:*",
|
|
57
|
+
"ec2:DescribeRegions",
|
|
58
|
+
"ec2:DescribeTransitGateways",
|
|
59
|
+
"ec2:DescribeVpnGateways",
|
|
60
|
+
"ecr-public:*",
|
|
61
|
+
"fms:*",
|
|
62
|
+
"freetier:*",
|
|
63
|
+
"globalaccelerator:*",
|
|
64
|
+
"health:*",
|
|
65
|
+
"iam:*",
|
|
66
|
+
"importexport:*",
|
|
67
|
+
"invoicing:*",
|
|
68
|
+
"iq:*",
|
|
69
|
+
"kms:*",
|
|
70
|
+
"license-manager:ListReceivedLicenses",
|
|
71
|
+
"lightsail:Get*",
|
|
72
|
+
"mobileanalytics:*",
|
|
73
|
+
"networkmanager:*",
|
|
74
|
+
"notifications-contacts:*",
|
|
75
|
+
"notifications:*",
|
|
76
|
+
"organizations:*",
|
|
77
|
+
"payments:*",
|
|
78
|
+
"pricing:*",
|
|
79
|
+
"purchase-orders:*",
|
|
80
|
+
"quicksight:DescribeAccountSubscription",
|
|
81
|
+
"resource-explorer-2:*",
|
|
82
|
+
"route53-recovery-cluster:*",
|
|
83
|
+
"route53-recovery-control-config:*",
|
|
84
|
+
"route53-recovery-readiness:*",
|
|
85
|
+
"route53:*",
|
|
86
|
+
"route53domains:*",
|
|
87
|
+
"s3:CreateMultiRegionAccessPoint",
|
|
88
|
+
"s3:DeleteMultiRegionAccessPoint",
|
|
89
|
+
"s3:DescribeMultiRegionAccessPointOperation",
|
|
90
|
+
"s3:GetAccountPublicAccessBlock",
|
|
91
|
+
"s3:GetBucketLocation",
|
|
92
|
+
"s3:GetBucketPolicyStatus",
|
|
93
|
+
"s3:GetBucketPublicAccessBlock",
|
|
94
|
+
"s3:GetMultiRegionAccessPoint",
|
|
95
|
+
"s3:GetMultiRegionAccessPointPolicy",
|
|
96
|
+
"s3:GetMultiRegionAccessPointPolicyStatus",
|
|
97
|
+
"s3:GetStorageLensConfiguration",
|
|
98
|
+
"s3:GetStorageLensDashboard",
|
|
99
|
+
"s3:ListAllMyBuckets",
|
|
100
|
+
"s3:ListMultiRegionAccessPoints",
|
|
101
|
+
"s3:ListStorageLensConfigurations",
|
|
102
|
+
"s3:PutAccountPublicAccessBlock",
|
|
103
|
+
"s3:PutMultiRegionAccessPointPolicy",
|
|
104
|
+
"savingsplans:*",
|
|
105
|
+
"shield:*",
|
|
106
|
+
"sso:*",
|
|
107
|
+
"sts:*",
|
|
108
|
+
"support-console:*",
|
|
109
|
+
"support:*",
|
|
110
|
+
"supportapp:*",
|
|
111
|
+
"supportplans:*",
|
|
112
|
+
"sustainability:*",
|
|
113
|
+
"tag:*",
|
|
114
|
+
"trustedadvisor:*",
|
|
115
|
+
"vendor-insights:ListEntitledSecurityProfiles",
|
|
116
|
+
"waf-regional:*",
|
|
117
|
+
"waf:*",
|
|
118
|
+
"wafv2:*"
|
|
119
|
+
];
|
|
18
120
|
function automationExemption() {
|
|
19
121
|
return {
|
|
20
122
|
ArnNotLike: {
|
|
@@ -62,23 +164,7 @@ function buildFoundationGuardrails(allowedRegions) {
|
|
|
62
164
|
{
|
|
63
165
|
Sid: "DenyOutsideAllowedRegions",
|
|
64
166
|
Effect: "Deny",
|
|
65
|
-
NotAction: [
|
|
66
|
-
"iam:*",
|
|
67
|
-
"sts:*",
|
|
68
|
-
"cloudfront:*",
|
|
69
|
-
"route53:*",
|
|
70
|
-
"organizations:*",
|
|
71
|
-
"support:*",
|
|
72
|
-
"budgets:*",
|
|
73
|
-
"ce:*",
|
|
74
|
-
"waf:*",
|
|
75
|
-
"wafv2:*",
|
|
76
|
-
"globalaccelerator:*",
|
|
77
|
-
"health:*",
|
|
78
|
-
"account:*",
|
|
79
|
-
"tag:*",
|
|
80
|
-
"trustedadvisor:*"
|
|
81
|
-
],
|
|
167
|
+
NotAction: [...GLOBAL_SERVICE_EXEMPTIONS],
|
|
82
168
|
Resource: "*",
|
|
83
169
|
Condition: {
|
|
84
170
|
StringNotEquals: {
|
|
@@ -49,6 +49,14 @@ export interface S3Props {
|
|
|
49
49
|
readonly removalPolicy?: "DESTROY" | "RETAIN";
|
|
50
50
|
/** Declarative bucket-policy statements appended to the bucket's resource policy. */
|
|
51
51
|
readonly resourcePolicyStatements?: ResourcePolicyStatement[];
|
|
52
|
+
/**
|
|
53
|
+
* The owning Fjall app's name, required by website-hosting buckets — their
|
|
54
|
+
* exports are keyed on (app, bucket) so sibling apps' identically-named
|
|
55
|
+
* buckets stay deployable side by side. `StorageFactory` fills it from the
|
|
56
|
+
* App (as `CdnFactory` does); pass it only for direct `new Storage(...)`
|
|
57
|
+
* construction.
|
|
58
|
+
*/
|
|
59
|
+
readonly appName?: string;
|
|
52
60
|
}
|
|
53
61
|
export interface StorageBuildProps extends S3Props {
|
|
54
62
|
readonly stackPlacement?: "storage" | "cdn" | "compute";
|
|
@@ -74,8 +74,9 @@ export class Storage extends Construct {
|
|
|
74
74
|
this.bucket = new S3Bucket(this, `${id}Bucket`, {
|
|
75
75
|
bucketName: props.bucketName,
|
|
76
76
|
// The bucket nests one level below, so its own id is `${id}Bucket` — the
|
|
77
|
-
// author's handle for `fjallBucket(<name>)` is THIS construct's id.
|
|
77
|
+
// author's handle for `fjallBucket(<app>, <name>)` is THIS construct's id.
|
|
78
78
|
aliasTargetName: props.bucketName ?? id,
|
|
79
|
+
appName: props.appName,
|
|
79
80
|
versioned: props.versioned,
|
|
80
81
|
encryption: toBucketEncryption(props.encryption),
|
|
81
82
|
encryptionKey,
|
|
@@ -175,9 +176,15 @@ export class Storage extends Construct {
|
|
|
175
176
|
export class StorageFactory {
|
|
176
177
|
static build(id, props = {}) {
|
|
177
178
|
const { stackPlacement, ...s3Props } = props;
|
|
178
|
-
const fn = (
|
|
179
|
+
const fn = (app, scope) => {
|
|
179
180
|
validateStorageProps(s3Props);
|
|
180
|
-
|
|
181
|
+
// The App is the only place the app name is known here; website-hosting
|
|
182
|
+
// buckets need it for their (app, bucket)-keyed exports — the same fill
|
|
183
|
+
// CdnFactory performs for Cdn.
|
|
184
|
+
return new Storage(scope, id, {
|
|
185
|
+
...s3Props,
|
|
186
|
+
appName: s3Props.appName ?? app.getName()
|
|
187
|
+
});
|
|
181
188
|
};
|
|
182
189
|
if (stackPlacement) {
|
|
183
190
|
fn.stackPlacement = stackPlacement;
|
|
@@ -32,12 +32,14 @@ export declare function fjallCdn(appName: string): FjallAliasTarget;
|
|
|
32
32
|
/**
|
|
33
33
|
* Target helper for a Fjall S3 static-site bucket.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* `appName` names the app that owns the bucket — the first segment of every
|
|
36
|
+
* target helper's key, exactly as `fjallApp`/`fjallCdn` take it. Requires that
|
|
37
|
+
* app's stack to publish exports `${safeApp}-${safeBucket}-WebsiteEndpoint`
|
|
38
|
+
* and `${safeApp}-${safeBucket}-WebsiteHostedZoneId` — emitted by
|
|
39
|
+
* website-enabled `S3` storage. Naming a pair with no such exports fails at
|
|
40
|
+
* CloudFormation execution, exactly as `fjallApp` documents.
|
|
39
41
|
*/
|
|
40
|
-
export declare function fjallBucket(bucketName: string): FjallAliasTarget;
|
|
42
|
+
export declare function fjallBucket(appName: string, bucketName: string): FjallAliasTarget;
|
|
41
43
|
/**
|
|
42
44
|
* Target helper for an arbitrary `(dnsName, hostedZoneId)` pair. Accepts
|
|
43
45
|
* literal strings or CDK tokens (e.g. a `CfnOutput.value`). No
|
|
@@ -43,15 +43,18 @@ export function fjallCdn(appName) {
|
|
|
43
43
|
/**
|
|
44
44
|
* Target helper for a Fjall S3 static-site bucket.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* `appName` names the app that owns the bucket — the first segment of every
|
|
47
|
+
* target helper's key, exactly as `fjallApp`/`fjallCdn` take it. Requires that
|
|
48
|
+
* app's stack to publish exports `${safeApp}-${safeBucket}-WebsiteEndpoint`
|
|
49
|
+
* and `${safeApp}-${safeBucket}-WebsiteHostedZoneId` — emitted by
|
|
50
|
+
* website-enabled `S3` storage. Naming a pair with no such exports fails at
|
|
51
|
+
* CloudFormation execution, exactly as `fjallApp` documents.
|
|
50
52
|
*/
|
|
51
|
-
export function fjallBucket(bucketName) {
|
|
52
|
-
const resolved = resolveBucketTarget(bucketName);
|
|
53
|
+
export function fjallBucket(appName, bucketName) {
|
|
54
|
+
const resolved = resolveBucketTarget(appName, bucketName);
|
|
53
55
|
return {
|
|
54
56
|
kind: "bucket",
|
|
57
|
+
appName,
|
|
55
58
|
bucketName,
|
|
56
59
|
bind(record, zone) {
|
|
57
60
|
return resolved.bind(record, zone);
|
|
@@ -15,8 +15,8 @@ import type { FjallTarget } from "../interfaces/domain.js";
|
|
|
15
15
|
* | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbDnsName` |
|
|
16
16
|
* | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbHostedZoneId` |
|
|
17
17
|
* | `cloudFront.ts` CDN stack | `resolveCdnTarget` | `<App>CdnDistributionDomainName` |
|
|
18
|
-
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket
|
|
19
|
-
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket
|
|
18
|
+
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<App>-<Bucket>-WebsiteEndpoint` |
|
|
19
|
+
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<App>-<Bucket>-WebsiteHostedZoneId` |
|
|
20
20
|
*
|
|
21
21
|
* Both sides derive the name from `@fjall/util`'s `…ExportName` helpers, which
|
|
22
22
|
* take the RAW app or bucket name and sanitise internally. Compose these
|
|
@@ -50,10 +50,13 @@ export declare function resolveCdnTarget(appName: string): IAliasRecordTarget;
|
|
|
50
50
|
/**
|
|
51
51
|
* Resolve a Fjall S3 static-site bucket to an `IAliasRecordTarget`.
|
|
52
52
|
*
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
53
|
+
* `appName` names the app whose stack exports the pair — required because the
|
|
54
|
+
* export is keyed on (app, bucket), never on the bucket alone (sibling apps'
|
|
55
|
+
* identically-named buckets must stay deployable side by side). `bucketName`
|
|
56
|
+
* is the bucket's name as written, dots and all — the helper strips what
|
|
57
|
+
* CloudFormation will not hold, and the producer strips the same.
|
|
55
58
|
*/
|
|
56
|
-
export declare function resolveBucketTarget(bucketName: string): IAliasRecordTarget;
|
|
59
|
+
export declare function resolveBucketTarget(appName: string, bucketName: string): IAliasRecordTarget;
|
|
57
60
|
/**
|
|
58
61
|
* Pure passthrough — accepts concrete DNS name + hosted-zone id (or CDK
|
|
59
62
|
* tokens) and forwards them unchanged. No `Fn.importValue` wrapping.
|
|
@@ -15,8 +15,8 @@ import { albDnsExportName, albHostedZoneIdExportName, cdnDomainExportName, bucke
|
|
|
15
15
|
* | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbDnsName` |
|
|
16
16
|
* | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbHostedZoneId` |
|
|
17
17
|
* | `cloudFront.ts` CDN stack | `resolveCdnTarget` | `<App>CdnDistributionDomainName` |
|
|
18
|
-
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket
|
|
19
|
-
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket
|
|
18
|
+
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<App>-<Bucket>-WebsiteEndpoint` |
|
|
19
|
+
* | `s3.ts` bucket stack | `resolveBucketTarget` | `<App>-<Bucket>-WebsiteHostedZoneId` |
|
|
20
20
|
*
|
|
21
21
|
* Both sides derive the name from `@fjall/util`'s `…ExportName` helpers, which
|
|
22
22
|
* take the RAW app or bucket name and sanitise internally. Compose these
|
|
@@ -69,12 +69,15 @@ export function resolveCdnTarget(appName) {
|
|
|
69
69
|
/**
|
|
70
70
|
* Resolve a Fjall S3 static-site bucket to an `IAliasRecordTarget`.
|
|
71
71
|
*
|
|
72
|
-
* `
|
|
73
|
-
*
|
|
72
|
+
* `appName` names the app whose stack exports the pair — required because the
|
|
73
|
+
* export is keyed on (app, bucket), never on the bucket alone (sibling apps'
|
|
74
|
+
* identically-named buckets must stay deployable side by side). `bucketName`
|
|
75
|
+
* is the bucket's name as written, dots and all — the helper strips what
|
|
76
|
+
* CloudFormation will not hold, and the producer strips the same.
|
|
74
77
|
*/
|
|
75
|
-
export function resolveBucketTarget(bucketName) {
|
|
76
|
-
const dnsName = Fn.importValue(bucketWebsiteEndpointExportName(bucketName));
|
|
77
|
-
const hostedZoneId = Fn.importValue(bucketWebsiteHostedZoneIdExportName(bucketName));
|
|
78
|
+
export function resolveBucketTarget(appName, bucketName) {
|
|
79
|
+
const dnsName = Fn.importValue(bucketWebsiteEndpointExportName(appName, bucketName));
|
|
80
|
+
const hostedZoneId = Fn.importValue(bucketWebsiteHostedZoneIdExportName(appName, bucketName));
|
|
78
81
|
return buildAliasTarget(dnsName, hostedZoneId);
|
|
79
82
|
}
|
|
80
83
|
/**
|
|
@@ -98,7 +101,7 @@ export function resolveTargetToDnsName(target) {
|
|
|
98
101
|
case "cdn":
|
|
99
102
|
return Fn.importValue(cdnDomainExportName(target.appName));
|
|
100
103
|
case "bucket":
|
|
101
|
-
return Fn.importValue(bucketWebsiteEndpointExportName(target.bucketName));
|
|
104
|
+
return Fn.importValue(bucketWebsiteEndpointExportName(target.appName, target.bucketName));
|
|
102
105
|
case "custom":
|
|
103
106
|
return target.dnsName;
|
|
104
107
|
default: {
|
|
@@ -63,7 +63,16 @@ export interface BuildClickHouseUserDataOptions {
|
|
|
63
63
|
serverSecretArn: string;
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The subset of the user-data options that config.xml generation actually
|
|
68
|
+
* reads. `dataVolumeSizeGb` is deliberately absent: it shapes the boot
|
|
69
|
+
* SCRIPT (the launch-template refresh trigger), not the rendered server
|
|
70
|
+
* config — demanding it here forced the XML parity tests to invent a value
|
|
71
|
+
* the render never consumes. `tls` is read for presence only (it toggles
|
|
72
|
+
* the secure-listener block); the ARNs inside it are boot-script concerns.
|
|
73
|
+
*/
|
|
74
|
+
export type ServerConfigXmlOptions = Pick<BuildClickHouseUserDataOptions, "backupBucketName" | "backupBucketRegion" | "instanceType" | "coldTier" | "tls">;
|
|
75
|
+
export declare function generateServerConfigXml(options: ServerConfigXmlOptions): string;
|
|
67
76
|
export interface GenerateUsersConfigXmlOptions {
|
|
68
77
|
schemaAdmin: ClickHouseSchemaAdmin;
|
|
69
78
|
profiles: Record<string, ProfileSpec>;
|
|
@@ -37,15 +37,27 @@ export interface S3BucketProps extends BucketProps {
|
|
|
37
37
|
publicReadAccess?: boolean;
|
|
38
38
|
websiteHosting?: WebsiteHostingConfig;
|
|
39
39
|
/**
|
|
40
|
-
* Name a `fjallBucket(<name>)` alias target uses to import this
|
|
41
|
-
* website exports. Defaults to `bucketName ?? id`, which is right
|
|
42
|
-
* direct `new S3Bucket(scope, "Assets", …)` — `id` is then the
|
|
43
|
-
* handle. A wrapper that nests the bucket under a derived id
|
|
44
|
-
* `Storage` builds its bucket at `${id}Bucket`, so without
|
|
45
|
-
* `fjallBucket("assets")` record would import
|
|
46
|
-
* against an exported
|
|
40
|
+
* Name a `fjallBucket(<app>, <name>)` alias target uses to import this
|
|
41
|
+
* bucket's website exports. Defaults to `bucketName ?? id`, which is right
|
|
42
|
+
* for a direct `new S3Bucket(scope, "Assets", …)` — `id` is then the
|
|
43
|
+
* author's own handle. A wrapper that nests the bucket under a derived id
|
|
44
|
+
* MUST pass this: `Storage` builds its bucket at `${id}Bucket`, so without
|
|
45
|
+
* it a `fjallBucket(app, "assets")` record would import
|
|
46
|
+
* `…-Assets-WebsiteEndpoint` against an exported
|
|
47
|
+
* `…-AssetsBucket-WebsiteEndpoint`.
|
|
47
48
|
*/
|
|
48
49
|
aliasTargetName?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The owning Fjall app's name — the other half of the website-export key
|
|
52
|
+
* (`bucketWebsiteEndpointExportName(appName, bucket)`). REQUIRED whenever
|
|
53
|
+
* `websiteHosting` is set: export names are account+region-global and every
|
|
54
|
+
* scaffolded app names its website bucket identically, so an unqualified
|
|
55
|
+
* export makes two website-bearing apps mutually undeployable. The
|
|
56
|
+
* `StorageFactory` path fills this from the App automatically (as
|
|
57
|
+
* `CdnFactory` does for `Cdn`); only direct `new S3Bucket`/`new Storage`
|
|
58
|
+
* construction needs to pass it.
|
|
59
|
+
*/
|
|
60
|
+
appName?: string;
|
|
49
61
|
/**
|
|
50
62
|
* Declarative bucket-policy statements, each appended via
|
|
51
63
|
* `addToResourcePolicy`. The TLS-only `enforceSSL` deny is always applied
|
|
@@ -18,7 +18,7 @@ function toResourcePolicyPrincipal(identifier) {
|
|
|
18
18
|
export class S3Bucket extends Bucket {
|
|
19
19
|
backupVaultTier;
|
|
20
20
|
constructor(scope, id, props = {}) {
|
|
21
|
-
const { websiteHosting, backupVaultTier, resourcePolicyStatements, aliasTargetName, ...cdkProps } = props;
|
|
21
|
+
const { websiteHosting, backupVaultTier, resourcePolicyStatements, aliasTargetName, appName, ...cdkProps } = props;
|
|
22
22
|
const isPublic = props.publicReadAccess === true || websiteHosting !== undefined;
|
|
23
23
|
const versioned = props.versioned ?? shouldAutoVersion(backupVaultTier);
|
|
24
24
|
const removalPolicy = props.removalPolicy ?? toRemovalPolicy(envAwareRemovalPolicyDefault());
|
|
@@ -74,6 +74,18 @@ export class S3Bucket extends Bucket {
|
|
|
74
74
|
Tags.of(this).add(SDK_PRE_EMPTY_TAG_KEY, "true");
|
|
75
75
|
}
|
|
76
76
|
if (websiteHosting) {
|
|
77
|
+
if (appName === undefined) {
|
|
78
|
+
// Refusing beats falling back to a bucket-only export name: the
|
|
79
|
+
// consumer (`resolveBucketTarget`) always imports the app-qualified
|
|
80
|
+
// pair, so an unqualified export could never be resolved — it would
|
|
81
|
+
// only surface at CloudFormation execution as "No export named …",
|
|
82
|
+
// or worse, collide with a sibling app's identically-named bucket.
|
|
83
|
+
throw new Error(`S3 bucket '${id}': websiteHosting requires 'appName' — the ` +
|
|
84
|
+
"website exports are keyed on (app, bucket) so two apps' " +
|
|
85
|
+
"identically-named buckets stay deployable side by side. " +
|
|
86
|
+
"App-managed paths (StorageFactory/CdnFactory) fill it " +
|
|
87
|
+
"automatically; pass appName explicitly for direct construction.");
|
|
88
|
+
}
|
|
77
89
|
const targetBucketName = aliasTargetName ?? props.bucketName ?? id;
|
|
78
90
|
// Only the export name routes through the contract helper. `safeBucket`
|
|
79
91
|
// stays local so the CfnOutput logical ids are unchanged — unifying the
|
|
@@ -81,7 +93,7 @@ export class S3Bucket extends Bucket {
|
|
|
81
93
|
const safeBucket = toPascalCase(targetBucketName.replace(/[^A-Za-z0-9-]/g, ""));
|
|
82
94
|
new CfnOutput(this, `${safeBucket}WebsiteEndpoint`, {
|
|
83
95
|
key: `${safeBucket}WebsiteEndpoint`,
|
|
84
|
-
exportName: bucketWebsiteEndpointExportName(targetBucketName),
|
|
96
|
+
exportName: bucketWebsiteEndpointExportName(appName, targetBucketName),
|
|
85
97
|
value: this.bucketWebsiteDomainName,
|
|
86
98
|
description: `S3 website endpoint for ${id} (consumed by Domain alias targets)`
|
|
87
99
|
});
|
|
@@ -93,7 +105,7 @@ export class S3Bucket extends Bucket {
|
|
|
93
105
|
}
|
|
94
106
|
new CfnOutput(this, `${safeBucket}WebsiteHostedZoneId`, {
|
|
95
107
|
key: `${safeBucket}WebsiteHostedZoneId`,
|
|
96
|
-
exportName: bucketWebsiteHostedZoneIdExportName(targetBucketName),
|
|
108
|
+
exportName: bucketWebsiteHostedZoneIdExportName(appName, targetBucketName),
|
|
97
109
|
value: websiteHostedZoneId,
|
|
98
110
|
description: `Route 53 hosted zone id for the S3 website endpoint of ${id}`
|
|
99
111
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "18.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
83
|
-
"@fjall/generator": "^
|
|
84
|
-
"@fjall/util": "^
|
|
83
|
+
"@fjall/generator": "^18.0.0",
|
|
84
|
+
"@fjall/util": "^18.0.0",
|
|
85
85
|
"constructs": "^10.7.2"
|
|
86
86
|
},
|
|
87
87
|
"overrides": {
|