@fjall/components-infrastructure 7.2.0 → 8.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.
Files changed (48) hide show
  1. package/dist/lib/app.d.ts +12 -0
  2. package/dist/lib/app.js +25 -5
  3. package/dist/lib/config/aws/ipam.js +4 -3
  4. package/dist/lib/patterns/aws/cdn.d.ts +7 -1
  5. package/dist/lib/patterns/aws/cdn.js +9 -2
  6. package/dist/lib/patterns/aws/database.js +4 -3
  7. package/dist/lib/patterns/aws/interfaces/domain.d.ts +6 -0
  8. package/dist/lib/patterns/aws/storage.js +3 -0
  9. package/dist/lib/patterns/aws/targets/fjallTargets.d.ts +9 -4
  10. package/dist/lib/patterns/aws/targets/fjallTargets.js +11 -5
  11. package/dist/lib/patterns/aws/targets/targetResolution.d.ts +33 -40
  12. package/dist/lib/patterns/aws/targets/targetResolution.js +42 -52
  13. package/dist/lib/resources/aws/backup/backupVault.js +2 -0
  14. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +9 -0
  15. package/dist/lib/resources/aws/cdn/cloudFront.js +2 -1
  16. package/dist/lib/resources/aws/compute/ecs.js +4 -3
  17. package/dist/lib/resources/aws/compute/ecsNetworking.js +29 -15
  18. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +2 -1
  19. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +6 -2
  20. package/dist/lib/resources/aws/database/rdsAurora.js +2 -2
  21. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +3 -2
  22. package/dist/lib/resources/aws/database/rdsHelpers.js +12 -2
  23. package/dist/lib/resources/aws/database/rdsInstance.js +3 -2
  24. package/dist/lib/resources/aws/database/rdsProxyOutput.js +2 -1
  25. package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
  26. package/dist/lib/resources/aws/networking/ipamPool.js +2 -1
  27. package/dist/lib/resources/aws/networking/vpc.js +8 -2
  28. package/dist/lib/resources/aws/secrets/kms.d.ts +24 -7
  29. package/dist/lib/resources/aws/secrets/kms.js +29 -6
  30. package/dist/lib/resources/aws/secrets/parameter.js +3 -1
  31. package/dist/lib/resources/aws/secrets/secret.js +5 -1
  32. package/dist/lib/resources/aws/storage/ecr.d.ts +6 -0
  33. package/dist/lib/resources/aws/storage/ecr.js +8 -4
  34. package/dist/lib/resources/aws/storage/s3.d.ts +10 -0
  35. package/dist/lib/resources/aws/storage/s3.js +9 -4
  36. package/dist/lib/utils/albAliasTargetRegistry.d.ts +44 -0
  37. package/dist/lib/utils/albAliasTargetRegistry.js +69 -0
  38. package/dist/lib/utils/env.d.ts +9 -0
  39. package/dist/lib/utils/env.js +12 -0
  40. package/dist/lib/utils/exportNaming.d.ts +50 -0
  41. package/dist/lib/utils/exportNaming.js +52 -0
  42. package/dist/lib/utils/orgConfigParser.d.ts +3 -8
  43. package/dist/lib/utils/orgConfigParser.js +4 -14
  44. package/dist/lib/utils/removalPolicy.d.ts +11 -3
  45. package/dist/lib/utils/removalPolicy.js +20 -5
  46. package/dist/lib/utils/resourceNaming.d.ts +50 -0
  47. package/dist/lib/utils/resourceNaming.js +74 -0
  48. package/package.json +9 -4
@@ -0,0 +1,69 @@
1
+ import { CfnOutput } from "aws-cdk-lib";
2
+ import { albDnsExportName, albHostedZoneIdExportName } from "@fjall/util";
3
+ import { toPascalCase } from "./capitaliseString.js";
4
+ import { FjallLogger } from "./validationLogger.js";
5
+ /**
6
+ * Keyed on the construct-tree root (the `App`) so two apps synthesised in one
7
+ * process — the platform and organisation stacks do this — never see each
8
+ * other's registrations. Weak so a discarded tree does not leak.
9
+ */
10
+ const registryByRoot = new WeakMap();
11
+ export function registerAlbAliasTarget(registration) {
12
+ const root = registration.scope.node.root;
13
+ const existing = registryByRoot.get(root);
14
+ if (existing === undefined) {
15
+ registryByRoot.set(root, [registration]);
16
+ return;
17
+ }
18
+ existing.push(registration);
19
+ }
20
+ /**
21
+ * Emit the unqualified app-level alias exports for every app in this tree
22
+ * that registered exactly one ALB. Call once, from `App.synth()`, before
23
+ * `super.synth()` — the construct tree is still mutable there, and that is
24
+ * the same window `applyTagsAspect()` uses.
25
+ */
26
+ export function emitAppLevelAlbAliasExports(root) {
27
+ const registrations = registryByRoot.get(root);
28
+ if (registrations === undefined)
29
+ return;
30
+ // `App.synth()` is re-entrant (CDK caches the assembly and returns it), and
31
+ // a second emission would collide on the output's construct id. Draining
32
+ // makes the call idempotent.
33
+ registryByRoot.delete(root);
34
+ const byApp = new Map();
35
+ for (const registration of registrations) {
36
+ const forApp = byApp.get(registration.appName);
37
+ if (forApp === undefined) {
38
+ byApp.set(registration.appName, [registration]);
39
+ }
40
+ else {
41
+ forApp.push(registration);
42
+ }
43
+ }
44
+ for (const [appName, forApp] of byApp) {
45
+ const only = forApp.length === 1 ? forApp[0] : undefined;
46
+ if (only === undefined) {
47
+ const computeNames = forApp.map((r) => r.computeName).join(", ");
48
+ FjallLogger.warn(`App '${appName}' fronts ${String(forApp.length)} load balancers (${computeNames}), ` +
49
+ `so no app-level '${albDnsExportName(appName)}' export was emitted — ` +
50
+ `'the app's ALB' would be ambiguous. Target a specific one with ` +
51
+ `fjallApp('${appName}', '<compute>') in your Domain records.`);
52
+ continue;
53
+ }
54
+ // Output logical ids are alphanumeric-only; the app name is not.
55
+ const logicalId = `${toPascalCase(appName)}AppAlb`;
56
+ new CfnOutput(only.scope, `${logicalId}DnsName`, {
57
+ key: `${logicalId}DnsName`,
58
+ exportName: albDnsExportName(appName),
59
+ value: only.dnsName,
60
+ description: `ALB DNS name for ${appName} (sole ALB; consumed by fjallApp('${appName}'))`
61
+ });
62
+ new CfnOutput(only.scope, `${logicalId}HostedZoneId`, {
63
+ key: `${logicalId}HostedZoneId`,
64
+ exportName: albHostedZoneIdExportName(appName),
65
+ value: only.hostedZoneId,
66
+ description: `ALB canonical hosted zone ID for ${appName} (sole ALB)`
67
+ });
68
+ }
69
+ }
@@ -47,6 +47,15 @@ export declare function getEnvironment(): string;
47
47
  * sentinel meaning "no signal at all".
48
48
  */
49
49
  export declare function hasExplicitEnvironmentSignal(): boolean;
50
+ /**
51
+ * True when this synth was driven by Fjall rather than run bare. deploy-core
52
+ * supplies `-c orgConfig=` on every synth it drives and nothing else does, so
53
+ * its presence separates "a developer is inspecting a template" from "an
54
+ * account is about to be deployed to" — a distinction consumers that pick a
55
+ * default on missing input need, because the safe default differs between the
56
+ * two. See `envAwareRemovalPolicyDefault`.
57
+ */
58
+ export declare function isFjallDrivenSynth(): boolean;
50
59
  /**
51
60
  * Resolve an environment-specific value at CDK synth time.
52
61
  *
@@ -77,6 +77,18 @@ export function hasExplicitEnvironmentSignal() {
77
77
  const ctxValue = parseContextArg("environment");
78
78
  return ctxValue !== undefined && ctxValue !== "";
79
79
  }
80
+ /**
81
+ * True when this synth was driven by Fjall rather than run bare. deploy-core
82
+ * supplies `-c orgConfig=` on every synth it drives and nothing else does, so
83
+ * its presence separates "a developer is inspecting a template" from "an
84
+ * account is about to be deployed to" — a distinction consumers that pick a
85
+ * default on missing input need, because the safe default differs between the
86
+ * two. See `envAwareRemovalPolicyDefault`.
87
+ */
88
+ export function isFjallDrivenSynth() {
89
+ const raw = parseContextArg(CDK_CONTEXT_KEYS.ORG_CONFIG);
90
+ return raw !== undefined && raw !== "";
91
+ }
80
92
  /**
81
93
  * Resolve an environment-specific value at CDK synth time.
82
94
  *
@@ -0,0 +1,50 @@
1
+ import type { Construct } from "constructs";
2
+ /**
3
+ * Qualify a CloudFormation export name with the stack that emits it.
4
+ *
5
+ * Export names are unique per account per region, so a name built only from a
6
+ * construct id or a cluster name is shared by every app that happens to use
7
+ * the same id — and the ids inside a pattern are literals the app author never
8
+ * chooses. Two apps then collide, and the second one fails at CloudFormation
9
+ * execution ("Export with name X is already exported by stack Y") after its
10
+ * first resources exist, so it rolls back rather than refusing up front.
11
+ *
12
+ * Stack names are already unique per account+region (CloudFormation enforces
13
+ * it) and Fjall derives them from the app name, so prefixing with the stack
14
+ * name makes the export globally unique AND puts the app name in it.
15
+ *
16
+ * The suffix must stay last. Consumers match these by suffix and by app-name
17
+ * token, never by whole string — `EcsServiceResolver` filters on
18
+ * `endsWith("deployableservice")` (deploy-core
19
+ * services/infrastructure/EcsServiceResolver.ts) and the webapp finds a linked
20
+ * app's CIDR by `ListExports` + a `-vpc-cidr` suffix filter
21
+ * (webapp app/.server/services/infrastructure/captureVpcCidr.ts). Prefixing is
22
+ * transparent to both; changing the tail is not.
23
+ *
24
+ * NOT for exports a consumer reconstructs by whole name. Those are contracts
25
+ * with their own vocabulary module and must keep their exact spelling:
26
+ * `getDomainExportNames` (@fjall/util), `EXPORT_NAMES`
27
+ * (patterns/aws/targets/targetResolution.ts), `SHARED_ALARM_TOPIC_EXPORT_NAME`,
28
+ * the CloudTrail output keys, and `${toPascalCase(appName)}EcrRepositoryName`
29
+ * (read by `resolveEcrRepositoryName` in the CLI). Account-level singletons
30
+ * (`AccountId`, `Environment`, `OrganisationId`, `FjallAuditRoleArn`, …) also
31
+ * keep their bare names: exactly one is meant to exist, so a collision there
32
+ * correctly reports a duplicate deployment.
33
+ */
34
+ export declare function stackScopedExportName(scope: Construct, suffix: string): string;
35
+ /**
36
+ * Export name for the account's IPAM private default scope.
37
+ *
38
+ * Deliberately unqualified: exactly one IPAM exists per account, so a
39
+ * collision here correctly reports a duplicate deployment rather than an
40
+ * accidental clash between two apps.
41
+ *
42
+ * It lives here rather than beside the construct that emits it because the
43
+ * producer is in `config/` and the consumer (`IpamPool`) is in `resources/`,
44
+ * which may not import from `config/`. Both sides must read the same
45
+ * constant: CloudFormation export names are case-sensitive, and the two sites
46
+ * previously disagreed on the leading capital — so the import could never
47
+ * resolve, and the only reason nothing broke is that every caller passed
48
+ * `ipamScope` explicitly and never reached the fallback.
49
+ */
50
+ export declare const IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME = "IpamPrivateDefaultScopeId";
@@ -0,0 +1,52 @@
1
+ import { Stack } from "aws-cdk-lib";
2
+ /**
3
+ * Qualify a CloudFormation export name with the stack that emits it.
4
+ *
5
+ * Export names are unique per account per region, so a name built only from a
6
+ * construct id or a cluster name is shared by every app that happens to use
7
+ * the same id — and the ids inside a pattern are literals the app author never
8
+ * chooses. Two apps then collide, and the second one fails at CloudFormation
9
+ * execution ("Export with name X is already exported by stack Y") after its
10
+ * first resources exist, so it rolls back rather than refusing up front.
11
+ *
12
+ * Stack names are already unique per account+region (CloudFormation enforces
13
+ * it) and Fjall derives them from the app name, so prefixing with the stack
14
+ * name makes the export globally unique AND puts the app name in it.
15
+ *
16
+ * The suffix must stay last. Consumers match these by suffix and by app-name
17
+ * token, never by whole string — `EcsServiceResolver` filters on
18
+ * `endsWith("deployableservice")` (deploy-core
19
+ * services/infrastructure/EcsServiceResolver.ts) and the webapp finds a linked
20
+ * app's CIDR by `ListExports` + a `-vpc-cidr` suffix filter
21
+ * (webapp app/.server/services/infrastructure/captureVpcCidr.ts). Prefixing is
22
+ * transparent to both; changing the tail is not.
23
+ *
24
+ * NOT for exports a consumer reconstructs by whole name. Those are contracts
25
+ * with their own vocabulary module and must keep their exact spelling:
26
+ * `getDomainExportNames` (@fjall/util), `EXPORT_NAMES`
27
+ * (patterns/aws/targets/targetResolution.ts), `SHARED_ALARM_TOPIC_EXPORT_NAME`,
28
+ * the CloudTrail output keys, and `${toPascalCase(appName)}EcrRepositoryName`
29
+ * (read by `resolveEcrRepositoryName` in the CLI). Account-level singletons
30
+ * (`AccountId`, `Environment`, `OrganisationId`, `FjallAuditRoleArn`, …) also
31
+ * keep their bare names: exactly one is meant to exist, so a collision there
32
+ * correctly reports a duplicate deployment.
33
+ */
34
+ export function stackScopedExportName(scope, suffix) {
35
+ return `${Stack.of(scope).stackName}${suffix}`;
36
+ }
37
+ /**
38
+ * Export name for the account's IPAM private default scope.
39
+ *
40
+ * Deliberately unqualified: exactly one IPAM exists per account, so a
41
+ * collision here correctly reports a duplicate deployment rather than an
42
+ * accidental clash between two apps.
43
+ *
44
+ * It lives here rather than beside the construct that emits it because the
45
+ * producer is in `config/` and the consumer (`IpamPool`) is in `resources/`,
46
+ * which may not import from `config/`. Both sides must read the same
47
+ * constant: CloudFormation export names are case-sensitive, and the two sites
48
+ * previously disagreed on the leading capital — so the import could never
49
+ * resolve, and the only reason nothing broke is that every caller passed
50
+ * `ipamScope` explicitly and never reached the fallback.
51
+ */
52
+ export const IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME = "IpamPrivateDefaultScopeId";
@@ -6,15 +6,10 @@ export interface ParsedOrgConfig {
6
6
  disasterRecoveryRegion?: string;
7
7
  }
8
8
  /**
9
- * Resolve a provider account's synth-time environment. The tier/stage
10
- * separation (decisions/2026-06-07-account-tier-vs-stage-separation.md) nulls
11
- * the wire `environment` for organisation-tier accounts, but scaffolded org
12
- * entry points gate on `config.environment === "root"` — decode the tier back
13
- * to the historical "root" marker via the sanctioned `accountTier()` decoder.
14
- * Workload stages pass through verbatim; a null stage on any other tier stays
15
- * unresolved (callers fall back to "unknown").
9
+ * Re-exported from `@fjall/util` so the synth-time reader and deploy-core's
10
+ * pre-synth resolver cannot drift. Do not redeclare it here.
16
11
  */
17
- export declare function resolveSynthEnvironment(account: Pick<ProviderAccount, "environment" | "tier">): string | undefined;
12
+ export { resolveSynthEnvironment } from "@fjall/util";
18
13
  /**
19
14
  * Parse orgConfig JSON from CDK context into a validated structure.
20
15
  *
@@ -1,5 +1,5 @@
1
1
  import { VAULT_LOCK_MODES, S3_BPA_MODES } from "@fjall/util/config";
2
- import { ACCOUNT_TIERS, STRUCTURAL_ENVIRONMENTS, accountTier, maskSensitiveOutput } from "@fjall/util";
2
+ import { ACCOUNT_TIERS, maskSensitiveOutput } from "@fjall/util";
3
3
  import { FjallLogger } from "./validationLogger.js";
4
4
  function isProviderAccount(item) {
5
5
  if (typeof item !== "object" || item === null)
@@ -20,20 +20,10 @@ function isProviderAccount(item) {
20
20
  typeof rec.acknowledgeImmutableVaultLock === "boolean"));
21
21
  }
22
22
  /**
23
- * Resolve a provider account's synth-time environment. The tier/stage
24
- * separation (decisions/2026-06-07-account-tier-vs-stage-separation.md) nulls
25
- * the wire `environment` for organisation-tier accounts, but scaffolded org
26
- * entry points gate on `config.environment === "root"` — decode the tier back
27
- * to the historical "root" marker via the sanctioned `accountTier()` decoder.
28
- * Workload stages pass through verbatim; a null stage on any other tier stays
29
- * unresolved (callers fall back to "unknown").
23
+ * Re-exported from `@fjall/util` so the synth-time reader and deploy-core's
24
+ * pre-synth resolver cannot drift. Do not redeclare it here.
30
25
  */
31
- export function resolveSynthEnvironment(account) {
32
- if (accountTier(account) === "organisation") {
33
- return STRUCTURAL_ENVIRONMENTS.ROOT;
34
- }
35
- return account.environment ?? undefined;
36
- }
26
+ export { resolveSynthEnvironment } from "@fjall/util";
37
27
  function describeRejectedAccount(item) {
38
28
  if (typeof item !== "object" || item === null)
39
29
  return "<unidentifiable>";
@@ -8,10 +8,18 @@ export declare function toRemovalPolicy(value?: "DESTROY" | "RETAIN" | "SNAPSHOT
8
8
  * benign), an unrecognised value here throws at synth: silently landing a
9
9
  * typo like `ENVIRONMENT=prod` on DESTROY deletes data when the stack is
10
10
  * deleted. The accept-set derives from `ACCOUNT_STAGES_WITH_ROOT` so a new
11
- * stage added in `@fjall/util` widens it automatically. The no-signal
12
- * sentinel keeps the historical warn-and-DESTROY behaviour — raw `cdk synth`
13
- * without context and null-stage cascade synths rely on it. An explicitly
11
+ * stage added in `@fjall/util` widens it automatically. An explicitly
14
12
  * supplied `ENVIRONMENT=unknown` collides with the sentinel string and would
15
13
  * otherwise ride the silent-DESTROY path, so it is treated as unrecognised.
14
+ *
15
+ * The no-signal sentinel keeps warn-and-DESTROY for a BARE `cdk synth`: a
16
+ * developer inspecting a template deploys nothing, so the policy it prints
17
+ * costs nothing. It is not a safe default for a synth that is about to
18
+ * deploy, so a Fjall-driven one (`isFjallDrivenSynth`) throws instead — there,
19
+ * "no signal" means the account resolved to no environment, and reading that
20
+ * as DESTROY is how a production stack ends up with `Delete` on every bucket.
21
+ * deploy-core resolves the environment before synth and passes it explicitly
22
+ * (deploy-core orchestration/deployEnvironment.ts), so a conforming deploy
23
+ * reaches neither branch; this is the backstop for one that does.
16
24
  */
17
25
  export declare function envAwareRemovalPolicyDefault(): "DESTROY" | "RETAIN";
@@ -1,6 +1,6 @@
1
1
  import { ACCOUNT_STAGES_WITH_ROOT } from "@fjall/util";
2
2
  import { RemovalPolicy } from "aws-cdk-lib";
3
- import { getEnvironment, hasExplicitEnvironmentSignal, UNKNOWN_ENVIRONMENT } from "./env.js";
3
+ import { getEnvironment, hasExplicitEnvironmentSignal, isFjallDrivenSynth, UNKNOWN_ENVIRONMENT } from "./env.js";
4
4
  export function toRemovalPolicy(value) {
5
5
  switch (value) {
6
6
  case "DESTROY":
@@ -21,16 +21,31 @@ const REMOVAL_DEFAULT_ENVIRONMENTS = new Set(ACCOUNT_STAGES_WITH_ROOT);
21
21
  * benign), an unrecognised value here throws at synth: silently landing a
22
22
  * typo like `ENVIRONMENT=prod` on DESTROY deletes data when the stack is
23
23
  * deleted. The accept-set derives from `ACCOUNT_STAGES_WITH_ROOT` so a new
24
- * stage added in `@fjall/util` widens it automatically. The no-signal
25
- * sentinel keeps the historical warn-and-DESTROY behaviour — raw `cdk synth`
26
- * without context and null-stage cascade synths rely on it. An explicitly
24
+ * stage added in `@fjall/util` widens it automatically. An explicitly
27
25
  * supplied `ENVIRONMENT=unknown` collides with the sentinel string and would
28
26
  * otherwise ride the silent-DESTROY path, so it is treated as unrecognised.
27
+ *
28
+ * The no-signal sentinel keeps warn-and-DESTROY for a BARE `cdk synth`: a
29
+ * developer inspecting a template deploys nothing, so the policy it prints
30
+ * costs nothing. It is not a safe default for a synth that is about to
31
+ * deploy, so a Fjall-driven one (`isFjallDrivenSynth`) throws instead — there,
32
+ * "no signal" means the account resolved to no environment, and reading that
33
+ * as DESTROY is how a production stack ends up with `Delete` on every bucket.
34
+ * deploy-core resolves the environment before synth and passes it explicitly
35
+ * (deploy-core orchestration/deployEnvironment.ts), so a conforming deploy
36
+ * reaches neither branch; this is the backstop for one that does.
29
37
  */
30
38
  export function envAwareRemovalPolicyDefault() {
31
39
  const environment = getEnvironment();
32
40
  if (environment === UNKNOWN_ENVIRONMENT && !hasExplicitEnvironmentSignal()) {
33
- return "DESTROY";
41
+ if (!isFjallDrivenSynth())
42
+ return "DESTROY";
43
+ throw new Error(`Refusing to resolve the env-aware removal-policy default: this deploy ` +
44
+ `carries an organisation config, but the account it targets resolved ` +
45
+ `to no environment. Defaulting to DESTROY here would delete data when ` +
46
+ `the stack is deleted. Set the target account's environment in the ` +
47
+ `organisation config, or pass -c environment=<value> explicitly. ` +
48
+ `Valid values: ${ACCOUNT_STAGES_WITH_ROOT.join(", ")}.`);
34
49
  }
35
50
  if (!REMOVAL_DEFAULT_ENVIRONMENTS.has(environment)) {
36
51
  throw new Error(`Unrecognised environment "${environment}" — refusing to resolve the ` +
@@ -41,4 +41,54 @@ export declare class ResourceNaming {
41
41
  static proxySecurityGroupDescription(name: string): string;
42
42
  /** Security group description for read replica. */
43
43
  static readReplicaSecurityGroupDescription(name: string): string;
44
+ /**
45
+ * KMS alias for a database's storage encryption key. `name` is the logical
46
+ * database name, and the casing is load-bearing — KMS aliases are
47
+ * case-sensitive and these are live on deployed keys.
48
+ *
49
+ * The three alias helpers return the value handed to `CustomerManagedKey`,
50
+ * which is what CDK prefixes with `alias/`; `kms list-aliases` reports the
51
+ * prefixed form.
52
+ */
53
+ static storageEncryptionKeyAlias(name: string): string;
54
+ /** KMS alias for a database's Database Insights encryption key. */
55
+ static insightsKeyAlias(name: string): string;
56
+ /** KMS alias for a read replica's Database Insights encryption key. */
57
+ static readReplicaInsightsKeyAlias(name: string): string;
58
+ /**
59
+ * Construct id of the RDS child the Database pattern creates for an
60
+ * `Instance` or `Aurora` plan. The instance, cluster, proxy and secret
61
+ * helpers are all fed this id, NOT the Database construct's own id, so
62
+ * anything predicting a deployed name from a resource plan starts here.
63
+ */
64
+ static rdsChildConstructId(databaseName: string): string;
65
+ /** Construct id of the RdsAuroraGlobal child, which names the cluster. */
66
+ static globalRdsChildConstructId(databaseName: string): string;
67
+ /**
68
+ * Construct id of the RdsAurora primary inside RdsAuroraGlobal, which names
69
+ * its instances, proxy and credentials secret.
70
+ */
71
+ static globalPrimaryConstructId(databaseName: string): string;
72
+ /** Construct id of a database's credentials Secret. */
73
+ static credentialsSecretConstructId(databaseName: string): string;
74
+ /**
75
+ * Alias a Secret or CustomerManagedKey takes when the caller supplies none.
76
+ * CDK prefixes it with `alias/`, which is the form `kms list-aliases`
77
+ * reports.
78
+ */
79
+ static defaultKeyAlias(constructId: string): string;
80
+ /**
81
+ * S3 bucket holding a VPC's flow logs. `id` is the Network construct id —
82
+ * the network name for an additional VPC, `${stackPrefix}Vpc` for the
83
+ * primary. Lowercased rather than kebabed: S3 forbids uppercase and this
84
+ * name is already live on deployed buckets, so the transform cannot change.
85
+ */
86
+ static vpcFlowLogBucketName(id: string, accountId: string): string;
87
+ /**
88
+ * ALB physical name for an ECS cluster. ELBv2 caps names at 32 characters
89
+ * and rejects a trailing hyphen, so a long cluster name is truncated and
90
+ * then stripped — which means the name is NOT simply
91
+ * `${clusterName}LoadBalancer` and must not be re-derived as if it were.
92
+ */
93
+ static loadBalancerName(clusterName: string): string;
44
94
  }
@@ -1,4 +1,6 @@
1
1
  import { toKebab } from "./capitaliseString.js";
2
+ /** ELBv2's hard limit on an Application Load Balancer's name. */
3
+ const LOAD_BALANCER_NAME_MAX_LENGTH = 32;
2
4
  /**
3
5
  * Centralised resource naming for infrastructure.
4
6
  * Ensures consistent, predictable names across constructs and tests.
@@ -73,4 +75,76 @@ export class ResourceNaming {
73
75
  static readReplicaSecurityGroupDescription(name) {
74
76
  return `Security group for RDS Read Replica ${name}`;
75
77
  }
78
+ /**
79
+ * KMS alias for a database's storage encryption key. `name` is the logical
80
+ * database name, and the casing is load-bearing — KMS aliases are
81
+ * case-sensitive and these are live on deployed keys.
82
+ *
83
+ * The three alias helpers return the value handed to `CustomerManagedKey`,
84
+ * which is what CDK prefixes with `alias/`; `kms list-aliases` reports the
85
+ * prefixed form.
86
+ */
87
+ static storageEncryptionKeyAlias(name) {
88
+ return `cmk/rds/${name}/encryptionKey`;
89
+ }
90
+ /** KMS alias for a database's Database Insights encryption key. */
91
+ static insightsKeyAlias(name) {
92
+ return `cmk/rds/${name}/InsightsKey`;
93
+ }
94
+ /** KMS alias for a read replica's Database Insights encryption key. */
95
+ static readReplicaInsightsKeyAlias(name) {
96
+ return `cmk/rds/${name}/ReadReplicaInsightsKey`;
97
+ }
98
+ /**
99
+ * Construct id of the RDS child the Database pattern creates for an
100
+ * `Instance` or `Aurora` plan. The instance, cluster, proxy and secret
101
+ * helpers are all fed this id, NOT the Database construct's own id, so
102
+ * anything predicting a deployed name from a resource plan starts here.
103
+ */
104
+ static rdsChildConstructId(databaseName) {
105
+ return `${databaseName}Rds`;
106
+ }
107
+ /** Construct id of the RdsAuroraGlobal child, which names the cluster. */
108
+ static globalRdsChildConstructId(databaseName) {
109
+ return `${databaseName}RdsGlobal`;
110
+ }
111
+ /**
112
+ * Construct id of the RdsAurora primary inside RdsAuroraGlobal, which names
113
+ * its instances, proxy and credentials secret.
114
+ */
115
+ static globalPrimaryConstructId(databaseName) {
116
+ return `${databaseName}Primary`;
117
+ }
118
+ /** Construct id of a database's credentials Secret. */
119
+ static credentialsSecretConstructId(databaseName) {
120
+ return `${databaseName}Credentials`;
121
+ }
122
+ /**
123
+ * Alias a Secret or CustomerManagedKey takes when the caller supplies none.
124
+ * CDK prefixes it with `alias/`, which is the form `kms list-aliases`
125
+ * reports.
126
+ */
127
+ static defaultKeyAlias(constructId) {
128
+ return `cmk/${constructId}`;
129
+ }
130
+ /**
131
+ * S3 bucket holding a VPC's flow logs. `id` is the Network construct id —
132
+ * the network name for an additional VPC, `${stackPrefix}Vpc` for the
133
+ * primary. Lowercased rather than kebabed: S3 forbids uppercase and this
134
+ * name is already live on deployed buckets, so the transform cannot change.
135
+ */
136
+ static vpcFlowLogBucketName(id, accountId) {
137
+ return `vpc-flowlogs-${id.toLowerCase()}-${accountId}`;
138
+ }
139
+ /**
140
+ * ALB physical name for an ECS cluster. ELBv2 caps names at 32 characters
141
+ * and rejects a trailing hyphen, so a long cluster name is truncated and
142
+ * then stripped — which means the name is NOT simply
143
+ * `${clusterName}LoadBalancer` and must not be re-derived as if it were.
144
+ */
145
+ static loadBalancerName(clusterName) {
146
+ return `${clusterName}LoadBalancer`
147
+ .slice(0, LOAD_BALANCER_NAME_MAX_LENGTH)
148
+ .replace(/-+$/, "");
149
+ }
76
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "7.2.0",
3
+ "version": "8.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -47,10 +47,15 @@
47
47
  "test:watch": "vitest",
48
48
  "contract:manifest": "FJALL_WRITE_CONTRACT_MANIFEST=1 vitest run lib/__tests__/_contract/contract.test.ts",
49
49
  "cdk": "cdk",
50
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
50
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.deploy-tests.json",
51
51
  "typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
52
+ "typecheck:deploy-tests": "tsc --noEmit -p tsconfig.deploy-tests.json",
52
53
  "check:scripts": "tsc --project tsconfig.scripts.json",
53
54
  "deploy-test": "tsx deploy-tests/run.mts",
55
+ "deploy-test:ops": "tsx deploy-tests/ops.mts",
56
+ "deploy-test:select": "tsx deploy-tests/select.mts",
57
+ "deploy-test:pipeline": "tsx deploy-tests/pipeline/renderPipelineSteps.mts",
58
+ "deploy-test:pipeline:check": "tsx deploy-tests/pipeline/renderPipelineSteps.mts --check",
54
59
  "lint": "eslint lib --no-warn-ignored",
55
60
  "lint:fix": "eslint lib --fix --no-warn-ignored",
56
61
  "format": "prettier --write \"lib/**/*.{ts,tsx,js,jsx,json}\" \"scripts/**/*.mts\" \"deploy-tests/**/*.mts\"",
@@ -74,8 +79,8 @@
74
79
  },
75
80
  "dependencies": {
76
81
  "@aws-sdk/client-organizations": "^3.1098.0",
77
- "@fjall/generator": "^7.2.0",
78
- "@fjall/util": "^7.2.0",
82
+ "@fjall/generator": "^8.0.0",
83
+ "@fjall/util": "^8.0.0",
79
84
  "constructs": "^10.7.2"
80
85
  },
81
86
  "overrides": {