@fjall/components-infrastructure 7.3.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 (44) 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/interfaces/domain.d.ts +6 -0
  7. package/dist/lib/patterns/aws/storage.js +3 -0
  8. package/dist/lib/patterns/aws/targets/fjallTargets.d.ts +9 -4
  9. package/dist/lib/patterns/aws/targets/fjallTargets.js +11 -5
  10. package/dist/lib/patterns/aws/targets/targetResolution.d.ts +33 -40
  11. package/dist/lib/patterns/aws/targets/targetResolution.js +42 -52
  12. package/dist/lib/resources/aws/backup/backupVault.js +2 -0
  13. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +9 -0
  14. package/dist/lib/resources/aws/cdn/cloudFront.js +2 -1
  15. package/dist/lib/resources/aws/compute/ecs.js +4 -3
  16. package/dist/lib/resources/aws/compute/ecsNetworking.js +27 -9
  17. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +2 -1
  18. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +6 -2
  19. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +2 -1
  20. package/dist/lib/resources/aws/database/rdsHelpers.js +12 -2
  21. package/dist/lib/resources/aws/database/rdsInstance.js +2 -1
  22. package/dist/lib/resources/aws/database/rdsProxyOutput.js +2 -1
  23. package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
  24. package/dist/lib/resources/aws/networking/ipamPool.js +2 -1
  25. package/dist/lib/resources/aws/networking/vpc.js +6 -1
  26. package/dist/lib/resources/aws/secrets/kms.d.ts +24 -7
  27. package/dist/lib/resources/aws/secrets/kms.js +27 -5
  28. package/dist/lib/resources/aws/secrets/parameter.js +3 -1
  29. package/dist/lib/resources/aws/secrets/secret.js +4 -1
  30. package/dist/lib/resources/aws/storage/ecr.d.ts +6 -0
  31. package/dist/lib/resources/aws/storage/ecr.js +8 -4
  32. package/dist/lib/resources/aws/storage/s3.d.ts +10 -0
  33. package/dist/lib/resources/aws/storage/s3.js +9 -4
  34. package/dist/lib/utils/albAliasTargetRegistry.d.ts +44 -0
  35. package/dist/lib/utils/albAliasTargetRegistry.js +69 -0
  36. package/dist/lib/utils/env.d.ts +9 -0
  37. package/dist/lib/utils/env.js +12 -0
  38. package/dist/lib/utils/exportNaming.d.ts +50 -0
  39. package/dist/lib/utils/exportNaming.js +52 -0
  40. package/dist/lib/utils/orgConfigParser.d.ts +3 -8
  41. package/dist/lib/utils/orgConfigParser.js +4 -14
  42. package/dist/lib/utils/removalPolicy.d.ts +11 -3
  43. package/dist/lib/utils/removalPolicy.js +20 -5
  44. package/package.json +5 -4
@@ -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 ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "7.3.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,8 +47,9 @@
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",
54
55
  "deploy-test:ops": "tsx deploy-tests/ops.mts",
@@ -78,8 +79,8 @@
78
79
  },
79
80
  "dependencies": {
80
81
  "@aws-sdk/client-organizations": "^3.1098.0",
81
- "@fjall/generator": "^7.3.0",
82
- "@fjall/util": "^7.3.0",
82
+ "@fjall/generator": "^8.0.0",
83
+ "@fjall/util": "^8.0.0",
83
84
  "constructs": "^10.7.2"
84
85
  },
85
86
  "overrides": {