@fjall/components-infrastructure 2.21.0 → 2.22.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.
@@ -2,6 +2,7 @@ import { Token } from "aws-cdk-lib";
2
2
  import { Runtime } from "aws-cdk-lib/aws-lambda";
3
3
  import { isCompute, isEcsCompute, isLambdaCompute, isEc2Compute } from "./interfaces/compute.js";
4
4
  import { warnIfPropertiesIgnored } from "../../utils/validationLogger.js";
5
+ import { applyInferredPublicBuildArgs } from "./computeBuildArgInference.js";
5
6
  import { DEFAULT_ECS_FALLBACK_IMAGE, DEFAULT_EC2_INSTANCE_TYPE } from "../../resources/aws/compute/ecsConstants.js";
6
7
  // Import and re-export from per-pattern files
7
8
  import { EcsCompute, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, resolveScalingConfig } from "./computeEcs.js";
@@ -163,6 +164,11 @@ export class ComputeFactory {
163
164
  const clusterName = id;
164
165
  const appName = computeProps.appName ?? app.getName();
165
166
  for (const service of computeProps.services) {
167
+ // Promote public-prefixed declared-env vars into docker.buildArgs
168
+ // (with their values) BEFORE the manifest copy and before the
169
+ // synth-time bake-guard runs in the EcsCompute constructor below —
170
+ // a secret-shaped inferred value must be caught by that guard.
171
+ applyInferredPublicBuildArgs(service);
166
172
  const manifestService = {
167
173
  name: service.name,
168
174
  clusterName
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Synth-time public build-arg inference (Phase 3, source 1).
3
+ *
4
+ * A Vite/Next service that lists `VITE_*` / `NEXT_PUBLIC_*` / `PUBLIC_*`
5
+ * variables in its container `environment` needs those values baked into the
6
+ * client bundle at BUILD time — container ENV cannot reach the client. Without
7
+ * inference the author has to duplicate every public var into `docker.buildArgs`
8
+ * by hand; a forgotten one silently ships an `undefined` to the browser.
9
+ *
10
+ * This helper promotes each public-prefixed declared-environment key into a
11
+ * `docker.buildArgs` entry, WRITING THROUGH its declared value (P3-D1) so it
12
+ * bakes even with no `.env` file present. It is a PROMOTE, not a move — the key
13
+ * legitimately stays a runtime env var too (harmless for client-public vars).
14
+ *
15
+ * Explicit `buildArgs` always win: an inferred key never overwrites an existing
16
+ * entry (which may carry an acknowledged public-but-sensitive object form).
17
+ *
18
+ * Pure — reads only the in-memory service config; no fs, no env reads. The
19
+ * returned `docker.buildArgs` is mutated in place on `service.docker` by the
20
+ * caller so the synth-time bake-guard (which reads `service.docker.buildArgs`)
21
+ * inspects the inferred values too.
22
+ */
23
+ import type { DockerBuildArgValue } from "@fjall/util/manifest/schemas";
24
+ interface ServiceWithDocker {
25
+ containers?: ReadonlyArray<{
26
+ environment?: Record<string, string>;
27
+ }>;
28
+ docker?: {
29
+ buildArgs?: Record<string, DockerBuildArgValue>;
30
+ };
31
+ }
32
+ /**
33
+ * Mutate `service.docker.buildArgs` in place to include every public-prefixed
34
+ * declared-environment key not already explicitly declared, writing through its
35
+ * declared value. No-op when the service has no `docker` config (nothing is
36
+ * built, so there are no build-args).
37
+ *
38
+ * Returns the (possibly mutated) `service.docker.buildArgs` for convenience;
39
+ * mutation is the load-bearing effect so the synth-time bake-guard sees the
40
+ * inferred values.
41
+ */
42
+ export declare function applyInferredPublicBuildArgs(service: ServiceWithDocker): void;
43
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Synth-time public build-arg inference (Phase 3, source 1).
3
+ *
4
+ * A Vite/Next service that lists `VITE_*` / `NEXT_PUBLIC_*` / `PUBLIC_*`
5
+ * variables in its container `environment` needs those values baked into the
6
+ * client bundle at BUILD time — container ENV cannot reach the client. Without
7
+ * inference the author has to duplicate every public var into `docker.buildArgs`
8
+ * by hand; a forgotten one silently ships an `undefined` to the browser.
9
+ *
10
+ * This helper promotes each public-prefixed declared-environment key into a
11
+ * `docker.buildArgs` entry, WRITING THROUGH its declared value (P3-D1) so it
12
+ * bakes even with no `.env` file present. It is a PROMOTE, not a move — the key
13
+ * legitimately stays a runtime env var too (harmless for client-public vars).
14
+ *
15
+ * Explicit `buildArgs` always win: an inferred key never overwrites an existing
16
+ * entry (which may carry an acknowledged public-but-sensitive object form).
17
+ *
18
+ * Pure — reads only the in-memory service config; no fs, no env reads. The
19
+ * returned `docker.buildArgs` is mutated in place on `service.docker` by the
20
+ * caller so the synth-time bake-guard (which reads `service.docker.buildArgs`)
21
+ * inspects the inferred values too.
22
+ */
23
+ import { inferPublicBuildArgKeys } from "@fjall/util/docker";
24
+ /**
25
+ * Aggregate the declared environment a service exposes across all its
26
+ * containers. Later containers win on key collision — the union is only used to
27
+ * discover public-prefixed KEYS and their values for write-through, so collision
28
+ * order is not load-bearing (a public var with two different values across
29
+ * containers is already an authoring error).
30
+ */
31
+ function aggregateDeclaredEnv(service) {
32
+ const merged = {};
33
+ for (const container of service.containers ?? []) {
34
+ if (container.environment === undefined)
35
+ continue;
36
+ for (const [key, value] of Object.entries(container.environment)) {
37
+ merged[key] = value;
38
+ }
39
+ }
40
+ return merged;
41
+ }
42
+ /**
43
+ * Mutate `service.docker.buildArgs` in place to include every public-prefixed
44
+ * declared-environment key not already explicitly declared, writing through its
45
+ * declared value. No-op when the service has no `docker` config (nothing is
46
+ * built, so there are no build-args).
47
+ *
48
+ * Returns the (possibly mutated) `service.docker.buildArgs` for convenience;
49
+ * mutation is the load-bearing effect so the synth-time bake-guard sees the
50
+ * inferred values.
51
+ */
52
+ export function applyInferredPublicBuildArgs(service) {
53
+ if (service.docker === undefined)
54
+ return;
55
+ const declaredEnv = aggregateDeclaredEnv(service);
56
+ const explicitBuildArgs = service.docker.buildArgs;
57
+ const inferredKeys = inferPublicBuildArgKeys({
58
+ declaredEnv,
59
+ ...(explicitBuildArgs !== undefined && { explicitBuildArgs })
60
+ });
61
+ if (inferredKeys.length === 0)
62
+ return;
63
+ const merged = {
64
+ ...(explicitBuildArgs ?? {})
65
+ };
66
+ for (const key of inferredKeys) {
67
+ // P3-D1: write through the declared value so it bakes even without a .env
68
+ // file. inferPublicBuildArgKeys already excluded explicit keys, so this
69
+ // never overwrites an explicit (possibly acknowledged) entry.
70
+ merged[key] = declaredEnv[key];
71
+ }
72
+ service.docker.buildArgs = merged;
73
+ }
@@ -22,6 +22,8 @@ import { vpcHasNatGateways } from "../../utils/vpcUtils.js";
22
22
  import { toPascalCase } from "../../utils/capitaliseString.js";
23
23
  import { FjallLogger } from "../../utils/validationLogger.js";
24
24
  import { VALIDATION_PATTERNS } from "@fjall/generator";
25
+ import { evaluateBakeGuard } from "@fjall/util/docker";
26
+ import { toKebab } from "@fjall/util";
25
27
  import { COMPUTE_DEFAULTS, collectImportedSecretNames } from "./compute.js";
26
28
  import { isHookMigrations } from "./computeEcsTypes.js";
27
29
  export { ScalingType } from "./computeEcsTypes.js";
@@ -93,6 +95,16 @@ export function validateEcsProps(props) {
93
95
  throw new Error(`Invalid service names: ${invalidNames.map((s) => s.name).join(", ")}. ` +
94
96
  "Service names must start with a letter and contain only letters, numbers, and hyphens.");
95
97
  }
98
+ // Reject secret-shaped buildArgs at synth time (mirror of the resources-layer
99
+ // validateEcsClusterProps check — direct `new EcsCluster(...)` consumers hit
100
+ // that one, EcsCompute/ComputeFactory consumers hit this one).
101
+ const appPrefix = props.appName !== undefined ? toKebab(props.appName) : "";
102
+ for (const service of props.services) {
103
+ const findings = evaluateBakeGuard(service.docker?.buildArgs, appPrefix).findings;
104
+ if (findings.length > 0) {
105
+ throw new Error(`Service '${service.name}': ${findings.map((f) => f.message).join(" ")}`);
106
+ }
107
+ }
96
108
  // Validate routing when multiple services have ports
97
109
  const servicesWithPorts = props.services.filter((s) => s.containers?.some((c) => c.port));
98
110
  if (servicesWithPorts.length > 1) {
@@ -1,4 +1,6 @@
1
1
  import { NetworkMode } from "aws-cdk-lib/aws-ecs";
2
+ import { evaluateBakeGuard } from "@fjall/util/docker";
3
+ import { toKebab } from "@fjall/util";
2
4
  import { ScalingType } from "./ecsTypes.js";
3
5
  /**
4
6
  * Validates ECS cluster props before construction.
@@ -36,6 +38,18 @@ export function validateEcsClusterProps(props) {
36
38
  if (duplicateServices.length > 0) {
37
39
  throw new Error(`Duplicate service names: ${[...new Set(duplicateServices)].join(", ")}`);
38
40
  }
41
+ // Reject secret-shaped buildArgs at synth time — a credential baked into a
42
+ // build arg lands in the image layer / docker history / provenance and the
43
+ // shared cache repo, where it survives long after the build. Fails the synth
44
+ // rather than deferring to the pre-build bake-guard, so a bad manifest never
45
+ // reaches a deploy. appName scopes the masking heuristic's prefix warnings.
46
+ const appPrefix = props.appName !== undefined ? toKebab(props.appName) : "";
47
+ for (const service of props.services) {
48
+ const findings = evaluateBakeGuard(service.docker?.buildArgs, appPrefix).findings;
49
+ if (findings.length > 0) {
50
+ throw new Error(`Service '${service.name}': ${findings.map((f) => f.message).join(" ")}`);
51
+ }
52
+ }
39
53
  // Validate routing when multiple services have ports
40
54
  const servicesWithPorts = props.services.filter((s) => s.containers.some((c) => c.port !== undefined));
41
55
  if (servicesWithPorts.length > 1 && !loadBalancerDisabled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "2.21.0",
3
+ "version": "2.22.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@aws-sdk/client-organizations": "^3.1038.0",
66
- "@fjall/generator": "^2.21.0",
67
- "@fjall/util": "^2.21.0",
66
+ "@fjall/generator": "^2.22.0",
67
+ "@fjall/util": "^2.22.0",
68
68
  "constructs": "^10.6.0"
69
69
  },
70
70
  "overrides": {
@@ -78,5 +78,5 @@
78
78
  "engines": {
79
79
  "node": ">=18.0.0"
80
80
  },
81
- "gitHead": "1b8902876c40697183ce8cbc25a0556b11d0ec38"
81
+ "gitHead": "448c771d258e87f0535ae7698f11438e0f7c57b1"
82
82
  }