@fjall/components-infrastructure 2.31.1 → 2.32.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 (26) hide show
  1. package/dist/lib/app.d.ts +27 -1
  2. package/dist/lib/app.js +35 -4
  3. package/dist/lib/patterns/aws/database.d.ts +19 -1
  4. package/dist/lib/patterns/aws/database.js +22 -3
  5. package/dist/lib/patterns/aws/devSubstrate.d.ts +85 -0
  6. package/dist/lib/patterns/aws/devSubstrate.js +390 -0
  7. package/dist/lib/patterns/aws/devSubstrate.waker.source.cjs +13 -0
  8. package/dist/lib/resources/aws/compute/applicationLoadBalancer.d.ts +17 -0
  9. package/dist/lib/resources/aws/compute/applicationLoadBalancer.js +23 -0
  10. package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +4 -10
  11. package/dist/lib/resources/aws/compute/ecsNetworking.js +13 -48
  12. package/dist/lib/resources/aws/compute/ecsRoles.d.ts +25 -0
  13. package/dist/lib/resources/aws/compute/ecsRoles.js +39 -20
  14. package/dist/lib/resources/aws/compute/hostHeaderListenerRule.d.ts +23 -0
  15. package/dist/lib/resources/aws/compute/hostHeaderListenerRule.js +59 -0
  16. package/dist/lib/resources/aws/compute/lambdaAlbTarget.d.ts +17 -0
  17. package/dist/lib/resources/aws/compute/lambdaAlbTarget.js +18 -0
  18. package/dist/lib/resources/aws/compute/listenerRouting.d.ts +21 -0
  19. package/dist/lib/resources/aws/compute/listenerRouting.js +20 -0
  20. package/dist/lib/resources/aws/database/rdsAurora.d.ts +16 -0
  21. package/dist/lib/resources/aws/database/rdsAurora.js +57 -3
  22. package/dist/lib/resources/aws/database/rdsDefaults.d.ts +28 -0
  23. package/dist/lib/resources/aws/database/rdsDefaults.js +28 -0
  24. package/dist/lib/utils/devSubstrateTags.d.ts +22 -0
  25. package/dist/lib/utils/devSubstrateTags.js +22 -0
  26. package/package.json +5 -5
@@ -2,12 +2,22 @@ import { Stack } from "aws-cdk-lib";
2
2
  import { Effect, Policy, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
3
3
  import { deriveSsmSecretsPath } from "./ecsTaskDefinition.js";
4
4
  /**
5
- * Creates the execution role for ECS infrastructure operations.
6
- * Used by the ECS agent to pull images, write logs, and inject secrets.
7
- * NOT used by application code that's the task role.
5
+ * Builds the base ECS task-execution role shared by every task the platform
6
+ * runs: the ECR-pull quartet (account-level, so `resources: ["*"]`) and a
7
+ * ViaService-conditioned `kms:Decrypt` for SSM SecureString / Secrets-Manager
8
+ * CMK env injection. Pass `ssmSecretsPath` (already derived — no partition/
9
+ * region/account prefix) to add the path-scoped `ssm:GetParameters` grant.
10
+ * Trust principal is `ecs-tasks.amazonaws.com`.
11
+ *
12
+ * Deliberately omits `logs:*` and `secretsmanager:GetSecretValue`: on the
13
+ * ECS-pattern path `AwsLogDriver.bind()` and `addContainer({ secrets })`
14
+ * auto-grant those on the exact LogGroup / secret ARNs. A caller that builds a
15
+ * SHARED or IMPORTED role those auto-grants cannot reach — e.g. the dev
16
+ * substrate's slot exec role, referenced by slot task-defs via ARN — MUST add
17
+ * the explicit `logs` / `secretsmanager` statements itself.
8
18
  */
9
- export function createExecutionRole(ctx, serviceName) {
10
- const executionRole = new Role(ctx.scope, `${serviceName}ExecutionRole`, {
19
+ export function createBaseExecutionRole(scope, id, opts = {}) {
20
+ const executionRole = new Role(scope, id, {
11
21
  assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com")
12
22
  });
13
23
  // GetAuthorizationToken is an account-level API that requires resources: ["*"].
@@ -23,25 +33,13 @@ export function createExecutionRole(ctx, serviceName) {
23
33
  ],
24
34
  resources: ["*"]
25
35
  }));
26
- const { partition, region, account } = Stack.of(ctx.scope);
27
- // Gotcha: no manual logs grant here. The task definition's AwsLogDriver
28
- // binds an explicit LogGroup construct (CFN auto-named — there is NO
29
- // `/ecs/{cluster}/...` group; that shape is only the stream prefix), and
30
- // `AwsLogDriver.bind()` auto-grants CreateLogStream/PutLogEvents on the
31
- // real group's ARN. A hand-rolled `/ecs/{cluster}*` statement grants
32
- // access to log groups that do not exist.
33
- // Gotcha: do NOT add a manual `secretsImport` grant here. `addContainer({ secrets })`
34
- // auto-grants `secret.grantRead(executionRole)` on the resolved complete ARN (exact, no
35
- // wildcard). A manual bare/`-*` statement is the 2026-06-04 outage shape and is dead.
36
- const serviceProps = ctx.props.services.find((s) => s.name === serviceName);
37
- const hasSsmSecrets = serviceProps?.containers.some((container) => container.secrets && container.secrets.length > 0) ?? false;
38
- if (hasSsmSecrets && serviceProps) {
39
- const ssmPath = deriveSsmSecretsPath(ctx.props, serviceName, serviceProps.ssmSecretsPath);
36
+ const { partition, region, account } = Stack.of(scope);
37
+ if (opts.ssmSecretsPath !== undefined) {
40
38
  executionRole.addToPolicy(new PolicyStatement({
41
39
  effect: Effect.ALLOW,
42
40
  actions: ["ssm:GetParameters", "ssm:GetParameter"],
43
41
  resources: [
44
- `arn:${partition}:ssm:${region}:${account}:parameter${ssmPath}/*`
42
+ `arn:${partition}:ssm:${region}:${account}:parameter${opts.ssmSecretsPath}/*`
45
43
  ]
46
44
  }));
47
45
  }
@@ -61,6 +59,27 @@ export function createExecutionRole(ctx, serviceName) {
61
59
  }));
62
60
  return executionRole;
63
61
  }
62
+ /**
63
+ * Creates the execution role for ECS infrastructure operations.
64
+ * Used by the ECS agent to pull images, write logs, and inject secrets.
65
+ * NOT used by application code — that's the task role.
66
+ *
67
+ * Delegates the ECR/kms/ssm shape to `createBaseExecutionRole`. Logs +
68
+ * secretsmanager are auto-granted downstream on this path (AwsLogDriver.bind()
69
+ * on the real LogGroup, addContainer({ secrets }) on the exact secret ARN), so a
70
+ * hand-rolled `/ecs/{cluster}*` logs or bare `-*` secrets statement here would be
71
+ * dead (the 2026-06-04 outage shape).
72
+ */
73
+ export function createExecutionRole(ctx, serviceName) {
74
+ const serviceProps = ctx.props.services.find((s) => s.name === serviceName);
75
+ const hasSsmSecrets = serviceProps?.containers.some((container) => container.secrets && container.secrets.length > 0) ?? false;
76
+ const ssmSecretsPath = hasSsmSecrets && serviceProps
77
+ ? deriveSsmSecretsPath(ctx.props, serviceName, serviceProps.ssmSecretsPath)
78
+ : undefined;
79
+ return createBaseExecutionRole(ctx.scope, `${serviceName}ExecutionRole`, {
80
+ ssmSecretsPath
81
+ });
82
+ }
64
83
  /**
65
84
  * Creates the task role for application code running in the container.
66
85
  * This role is assumed by the application, not the ECS agent.
@@ -0,0 +1,23 @@
1
+ import { ListenerCondition } from "aws-cdk-lib/aws-elasticloadbalancingv2";
2
+ import type { EcsRoutingConfig } from "./ecsTypes.js";
3
+ /**
4
+ * Mutable ALB rule-priority state shared across every rule on one listener. The
5
+ * ECS cluster path and the dev substrate both thread a single instance so
6
+ * auto-incremented and host-hashed priorities never collide on the same listener.
7
+ */
8
+ export interface PriorityState {
9
+ nextPriority: number;
10
+ usedPriorities: Set<number>;
11
+ }
12
+ /** Returns the next unused auto-incremented ALB priority, skipping any manually assigned values. */
13
+ export declare function getNextPriority(state: PriorityState): number;
14
+ export declare function buildRoutingConditions(rule: EcsRoutingConfig | undefined): ListenerCondition[];
15
+ /**
16
+ * A deterministic, in-band ALB rule priority for a host-routed slot rule. Same
17
+ * host → same priority across synths. Collisions (two hosts hashing equal, a
18
+ * host re-registering, or a manually-claimed priority) are resolved by
19
+ * open-addressing linear probe with wraparound within the band, claimed against
20
+ * the shared `usedPriorities`. Throws only if the (40 000-slot) band is exhausted
21
+ * — unreachable in practice given AWS's per-listener rule ceiling.
22
+ */
23
+ export declare function deterministicHostPriority(host: string, state: PriorityState): number;
@@ -0,0 +1,59 @@
1
+ import { ListenerCondition } from "aws-cdk-lib/aws-elasticloadbalancingv2";
2
+ /** Returns the next unused auto-incremented ALB priority, skipping any manually assigned values. */
3
+ export function getNextPriority(state) {
4
+ while (state.usedPriorities.has(state.nextPriority)) {
5
+ state.nextPriority++;
6
+ }
7
+ const priority = state.nextPriority++;
8
+ state.usedPriorities.add(priority);
9
+ return priority;
10
+ }
11
+ export function buildRoutingConditions(rule) {
12
+ const conditions = [];
13
+ if (rule?.path) {
14
+ conditions.push(ListenerCondition.pathPatterns([rule.path]));
15
+ }
16
+ if (rule?.host) {
17
+ conditions.push(ListenerCondition.hostHeaders([rule.host]));
18
+ }
19
+ return conditions;
20
+ }
21
+ // ALB rule priorities are 1..50000 and unique per listener. Host-hashed slot
22
+ // rules occupy a band DISJOINT from the auto-increment counter (which starts at
23
+ // 100 and climbs — realistically a few hundred at most), so a deterministic slot
24
+ // priority can never collide with an ECS-service rule that shares the same
25
+ // PriorityState. Band [10000, 49999] sits above the counter and below the 50000 max.
26
+ const HOST_PRIORITY_BAND_START = 10_000;
27
+ const HOST_PRIORITY_BAND_SIZE = 40_000;
28
+ /**
29
+ * FNV-1a 32-bit string hash. Deterministic across processes and machines (no
30
+ * Math.random / Date), so the same host maps to the same base priority on every
31
+ * synth — stable rule ordering, no spurious CloudFormation diffs.
32
+ */
33
+ function fnv1a32(input) {
34
+ let hash = 0x811c9dc5;
35
+ for (let i = 0; i < input.length; i++) {
36
+ hash ^= input.charCodeAt(i);
37
+ hash = Math.imul(hash, 0x0100_0193);
38
+ }
39
+ return hash >>> 0;
40
+ }
41
+ /**
42
+ * A deterministic, in-band ALB rule priority for a host-routed slot rule. Same
43
+ * host → same priority across synths. Collisions (two hosts hashing equal, a
44
+ * host re-registering, or a manually-claimed priority) are resolved by
45
+ * open-addressing linear probe with wraparound within the band, claimed against
46
+ * the shared `usedPriorities`. Throws only if the (40 000-slot) band is exhausted
47
+ * — unreachable in practice given AWS's per-listener rule ceiling.
48
+ */
49
+ export function deterministicHostPriority(host, state) {
50
+ const base = fnv1a32(host) % HOST_PRIORITY_BAND_SIZE;
51
+ for (let probe = 0; probe < HOST_PRIORITY_BAND_SIZE; probe++) {
52
+ const priority = HOST_PRIORITY_BAND_START + ((base + probe) % HOST_PRIORITY_BAND_SIZE);
53
+ if (!state.usedPriorities.has(priority)) {
54
+ state.usedPriorities.add(priority);
55
+ return priority;
56
+ }
57
+ }
58
+ throw new Error(`Exhausted the host-priority band resolving a priority for "${host}"`);
59
+ }
@@ -0,0 +1,17 @@
1
+ import { type IApplicationTargetGroup } from "aws-cdk-lib/aws-elasticloadbalancingv2";
2
+ import type { IFunction } from "aws-cdk-lib/aws-lambda";
3
+ import type { Construct } from "constructs";
4
+ export interface LambdaAlbTargetOptions {
5
+ readonly handler: IFunction;
6
+ }
7
+ /**
8
+ * Context-free Lambda-behind-ALB target group. Registering the handler as a
9
+ * `LambdaTarget` auto-adds the `elasticloadbalancing.amazonaws.com` invoke
10
+ * permission (CDK's `LambdaTarget.attachToApplicationTargetGroup` → `grantInvoke`),
11
+ * so no explicit `AWS::Lambda::Permission` is needed.
12
+ *
13
+ * Intentionally NOT attached to a listener here — the substrate exports the
14
+ * target-group ARN (A7) so slot-ops point a sleeping slot's host-header rule at
15
+ * it (§5.4); an unmatched host still falls through to the listener's 404.
16
+ */
17
+ export declare function createLambdaTargetGroup(scope: Construct, id: string, options: LambdaAlbTargetOptions): IApplicationTargetGroup;
@@ -0,0 +1,18 @@
1
+ import { ApplicationTargetGroup, TargetType } from "aws-cdk-lib/aws-elasticloadbalancingv2";
2
+ import { LambdaTarget } from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
3
+ /**
4
+ * Context-free Lambda-behind-ALB target group. Registering the handler as a
5
+ * `LambdaTarget` auto-adds the `elasticloadbalancing.amazonaws.com` invoke
6
+ * permission (CDK's `LambdaTarget.attachToApplicationTargetGroup` → `grantInvoke`),
7
+ * so no explicit `AWS::Lambda::Permission` is needed.
8
+ *
9
+ * Intentionally NOT attached to a listener here — the substrate exports the
10
+ * target-group ARN (A7) so slot-ops point a sleeping slot's host-header rule at
11
+ * it (§5.4); an unmatched host still falls through to the listener's 404.
12
+ */
13
+ export function createLambdaTargetGroup(scope, id, options) {
14
+ return new ApplicationTargetGroup(scope, id, {
15
+ targetType: TargetType.LAMBDA,
16
+ targets: [new LambdaTarget(options.handler)]
17
+ });
18
+ }
@@ -0,0 +1,21 @@
1
+ import { type ApplicationListener, type ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
2
+ import type { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
3
+ export interface RoutingListenerOptions {
4
+ readonly port: number;
5
+ readonly certificate?: ICertificate;
6
+ /**
7
+ * Attach a fixed 404 default action. The ECS path passes this conditionally
8
+ * (only when ≥2 routes exist or no service has a port — CDK rejects a listener
9
+ * with neither a default action nor target groups); the dev substrate passes
10
+ * `true` unconditionally, since its host-header rules are the only routes and
11
+ * an unmatched host must return 404, not fall through.
12
+ */
13
+ readonly default404: boolean;
14
+ }
15
+ /**
16
+ * Context-free ALB listener factory shared by the ECS cluster path and the dev
17
+ * substrate. When `default404` is set, the listener's default action is a plain
18
+ * `404 Not Found` fixed response; otherwise no default action is attached (the
19
+ * caller's target groups become the listener's default).
20
+ */
21
+ export declare function addRoutingListener(loadBalancer: ApplicationLoadBalancer, id: string, options: RoutingListenerOptions): ApplicationListener;
@@ -0,0 +1,20 @@
1
+ import { ListenerAction } from "aws-cdk-lib/aws-elasticloadbalancingv2";
2
+ /**
3
+ * Context-free ALB listener factory shared by the ECS cluster path and the dev
4
+ * substrate. When `default404` is set, the listener's default action is a plain
5
+ * `404 Not Found` fixed response; otherwise no default action is attached (the
6
+ * caller's target groups become the listener's default).
7
+ */
8
+ export function addRoutingListener(loadBalancer, id, options) {
9
+ const defaultAction = options.default404
10
+ ? ListenerAction.fixedResponse(404, {
11
+ contentType: "text/plain",
12
+ messageBody: "Not Found"
13
+ })
14
+ : undefined;
15
+ return loadBalancer.addListener(id, {
16
+ port: options.port,
17
+ ...(options.certificate && { certificates: [options.certificate] }),
18
+ ...(defaultAction !== undefined && { defaultAction })
19
+ });
20
+ }
@@ -10,8 +10,24 @@ interface RdsProps {
10
10
  vpc: IVpc;
11
11
  databaseName?: string;
12
12
  engine?: IClusterEngine;
13
+ /**
14
+ * Aurora PostgreSQL version string the `engine` was built at, carried purely
15
+ * so the auto-pause floor can be validated here (the layer that owns the
16
+ * min-capacity knob). `undefined` means the engine default (>= 16.3) is in use.
17
+ */
18
+ engineVersion?: string;
13
19
  engineConfig?: EngineConfig;
14
20
  clusterIdentifier?: string;
21
+ /** Physical name of the generated master-credentials secret. */
22
+ credentialsSecretName?: string;
23
+ /** Serverless-v2 floor in ACU. 0 enables scale-to-zero (auto-pause). */
24
+ serverlessV2MinCapacity?: number;
25
+ /** Serverless-v2 ceiling in ACU. */
26
+ serverlessV2MaxCapacity?: number;
27
+ /** Idle duration before an idle serverless-v2 cluster auto-pauses to 0 ACU. */
28
+ serverlessV2AutoPauseDuration?: Duration;
29
+ /** Enables the RDS Data API (HTTP endpoint) on the cluster. */
30
+ enableDataApi?: boolean;
15
31
  writer?: AuroraWriterConfig;
16
32
  readers?: AuroraReadersConfig | false;
17
33
  proxy?: ProxyConfig | false;
@@ -8,10 +8,49 @@ import { Secret } from "../secrets/index.js";
8
8
  import { ResourceNaming } from "../../../utils/resourceNaming.js";
9
9
  import { addProxyCfnOutput } from "./rdsProxyOutput.js";
10
10
  import { getDatabaseInsightsRetention } from "./index.js";
11
- import { RDS_DEFAULTS } from "./rdsDefaults.js";
11
+ import { RDS_DEFAULTS, SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS, SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS, MIN_AUTO_PAUSE_POSTGRES_VERSION } from "./rdsDefaults.js";
12
12
  import { createRdsAlarms } from "../monitoring/index.js";
13
13
  import { DEFAULT_POSTGRES_ENGINE_CONFIG, resolveDatabaseInsights, resolveStorageEncryptionKey, resolvePerformanceInsightsKey, addMultiUserSecretRotation, warnIfSnapshotUsernameAssumed } from "./rdsHelpers.js";
14
14
  import { resolveSecretRotation } from "../../../utils/databaseTypes.js";
15
+ function parsePostgresVersion(version) {
16
+ const parts = version.split(".");
17
+ return {
18
+ major: parseInt(parts[0] ?? "", 10),
19
+ minor: parseInt(parts[1] ?? "0", 10)
20
+ };
21
+ }
22
+ function assertAutoPauseCapablePostgresVersion(engineVersion) {
23
+ const { major, minor } = parsePostgresVersion(engineVersion);
24
+ if (Number.isNaN(major) || Number.isNaN(minor)) {
25
+ throw new Error(`Invalid Aurora engineVersion "${engineVersion}" (expected "<major>.<minor>")`);
26
+ }
27
+ const floor = parsePostgresVersion(MIN_AUTO_PAUSE_POSTGRES_VERSION);
28
+ const meetsFloor = major > floor.major || (major === floor.major && minor >= floor.minor);
29
+ if (!meetsFloor) {
30
+ throw new Error(`serverlessV2MinCapacity 0 requires Aurora PostgreSQL >= ${MIN_AUTO_PAUSE_POSTGRES_VERSION} for auto-pause; received engineVersion "${engineVersion}"`);
31
+ }
32
+ }
33
+ function validateServerlessV2DevKnobs(props) {
34
+ const min = props.serverlessV2MinCapacity;
35
+ const max = props.serverlessV2MaxCapacity;
36
+ if (min !== undefined && max !== undefined && min > max) {
37
+ throw new Error(`serverlessV2MinCapacity (${min}) must be <= serverlessV2MaxCapacity (${max})`);
38
+ }
39
+ if (props.serverlessV2AutoPauseDuration !== undefined) {
40
+ const seconds = props.serverlessV2AutoPauseDuration.toSeconds();
41
+ if (seconds < SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS ||
42
+ seconds > SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS) {
43
+ throw new Error(`serverlessV2AutoPauseDuration must be between ${SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS}s and ${SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS}s; received ${seconds}s`);
44
+ }
45
+ }
46
+ // Floor enforced only via engineVersion (opaque `IClusterEngine` can't be
47
+ // introspected): a raw `engine` below the floor with min 0 and no version is
48
+ // the caller's own risk. No such caller exists — DevSubstrate always passes
49
+ // engineVersion.
50
+ if (min === 0 && props.engineVersion !== undefined) {
51
+ assertAutoPauseCapablePostgresVersion(props.engineVersion);
52
+ }
53
+ }
15
54
  export class RdsAurora extends Construct {
16
55
  connections;
17
56
  constructId;
@@ -25,6 +64,7 @@ export class RdsAurora extends Construct {
25
64
  databaseNameValue;
26
65
  constructor(scope, id, props) {
27
66
  super(scope, id);
67
+ validateServerlessV2DevKnobs(props);
28
68
  this.constructId = id;
29
69
  this.databaseNameValue = props.databaseName ?? id;
30
70
  // PostgreSQL fallback for direct usage - ensure engine and engineConfig match
@@ -44,7 +84,8 @@ export class RdsAurora extends Construct {
44
84
  // Global secondary clusters import replicated secret instead of creating new
45
85
  if (props.isGlobalSecondary) {
46
86
  this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
47
- secretName: ResourceNaming.credentialsSecretName(id),
87
+ secretName: props.credentialsSecretName ??
88
+ ResourceNaming.credentialsSecretName(id),
48
89
  importExisting: true
49
90
  });
50
91
  }
@@ -57,7 +98,8 @@ export class RdsAurora extends Construct {
57
98
  assumedUsername: username
58
99
  });
59
100
  this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
60
- secretName: ResourceNaming.credentialsSecretName(id),
101
+ secretName: props.credentialsSecretName ??
102
+ ResourceNaming.credentialsSecretName(id),
61
103
  generateSecretString: {
62
104
  secretStringTemplate: JSON.stringify({ username }),
63
105
  excludePunctuation: true,
@@ -140,6 +182,18 @@ export class RdsAurora extends Construct {
140
182
  removalPolicy: RemovalPolicy.SNAPSHOT,
141
183
  deletionProtection: props.deletionProtection ?? true,
142
184
  iamAuthentication: true,
185
+ ...(props.serverlessV2MinCapacity !== undefined && {
186
+ serverlessV2MinCapacity: props.serverlessV2MinCapacity
187
+ }),
188
+ ...(props.serverlessV2MaxCapacity !== undefined && {
189
+ serverlessV2MaxCapacity: props.serverlessV2MaxCapacity
190
+ }),
191
+ ...(props.serverlessV2AutoPauseDuration !== undefined && {
192
+ serverlessV2AutoPauseDuration: props.serverlessV2AutoPauseDuration
193
+ }),
194
+ ...(props.enableDataApi !== undefined && {
195
+ enableDataApi: props.enableDataApi
196
+ }),
143
197
  writer,
144
198
  readers
145
199
  };
@@ -11,3 +11,31 @@ export declare const RDS_DEFAULTS: Readonly<{
11
11
  /** Default storage autoscaling ceiling (GiB) — applied to primary and replica */
12
12
  readonly DEFAULT_MAX_ALLOCATED_STORAGE_GIB: 500;
13
13
  }>;
14
+ /**
15
+ * Serverless-v2 defaults for the internal dev substrate's shared Aurora cluster
16
+ * (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
17
+ * idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
18
+ * is the only mode under which min-0 actually saves (a live pooled connection
19
+ * pins the cluster above 0 ACU). Engine 16.6 satisfies the auto-pause-capable
20
+ * floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`).
21
+ */
22
+ export declare const DEV_AURORA_DEFAULTS: Readonly<{
23
+ readonly SERVERLESS_V2_MIN_CAPACITY: 0;
24
+ readonly SERVERLESS_V2_MAX_CAPACITY: 4;
25
+ readonly AUTO_PAUSE_SECONDS: 1200;
26
+ readonly ENABLE_DATA_API: true;
27
+ readonly ENGINE_VERSION: "16.6";
28
+ }>;
29
+ /**
30
+ * Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
31
+ * (5 minutes – 24 hours). A value outside this range synths but is rejected at
32
+ * deploy, so the resources layer validates it at synth instead.
33
+ */
34
+ export declare const SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS = 300;
35
+ export declare const SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS = 86400;
36
+ /**
37
+ * Minimum Aurora PostgreSQL version that supports serverless-v2 auto-pause
38
+ * (`serverlessV2MinCapacity` 0). An explicit override below this synths clean
39
+ * but AccessDenies / rejects at deploy, so the resources layer hard-guards it.
40
+ */
41
+ export declare const MIN_AUTO_PAUSE_POSTGRES_VERSION = "16.3";
@@ -11,3 +11,31 @@ export const RDS_DEFAULTS = Object.freeze({
11
11
  /** Default storage autoscaling ceiling (GiB) — applied to primary and replica */
12
12
  DEFAULT_MAX_ALLOCATED_STORAGE_GIB: 500
13
13
  });
14
+ /**
15
+ * Serverless-v2 defaults for the internal dev substrate's shared Aurora cluster
16
+ * (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
17
+ * idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
18
+ * is the only mode under which min-0 actually saves (a live pooled connection
19
+ * pins the cluster above 0 ACU). Engine 16.6 satisfies the auto-pause-capable
20
+ * floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`).
21
+ */
22
+ export const DEV_AURORA_DEFAULTS = Object.freeze({
23
+ SERVERLESS_V2_MIN_CAPACITY: 0,
24
+ SERVERLESS_V2_MAX_CAPACITY: 4,
25
+ AUTO_PAUSE_SECONDS: 1200,
26
+ ENABLE_DATA_API: true,
27
+ ENGINE_VERSION: "16.6"
28
+ });
29
+ /**
30
+ * Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
31
+ * (5 minutes – 24 hours). A value outside this range synths but is rejected at
32
+ * deploy, so the resources layer validates it at synth instead.
33
+ */
34
+ export const SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS = 300;
35
+ export const SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS = 86400;
36
+ /**
37
+ * Minimum Aurora PostgreSQL version that supports serverless-v2 auto-pause
38
+ * (`serverlessV2MinCapacity` 0). An explicit override below this synths clean
39
+ * but AccessDenies / rejects at deploy, so the resources layer hard-guards it.
40
+ */
41
+ export const MIN_AUTO_PAUSE_POSTGRES_VERSION = "16.3";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Tags applied by the internal `DevSubstrate` construct.
3
+ *
4
+ * `fjall:dev = "true"` is IAM-load-bearing, not cosmetic. The shipped dev-tier
5
+ * deploy fence in `@fjall/generator` (`src/schemas/devRolePolicies.ts`) gates its
6
+ * slot-ops grants on `aws:ResourceTag/fjall:dev = "true"` and
7
+ * `aws:RequestTag/fjall:dev = "true"`. The key and value here MUST stay
8
+ * byte-identical to those IAM condition keys — any drift silently unfences the
9
+ * substrate (the runtime-name-must-match-construct coupling class).
10
+ *
11
+ * A util-level single source shared with the policy is a candidate follow-up; it
12
+ * is deliberately NOT folded here, to avoid editing the parity-locked security
13
+ * policy from a construct-scaffold change.
14
+ */
15
+ export declare const DEV_TAG_KEY: "fjall:dev";
16
+ export declare const DEV_TAG_VALUE: "true";
17
+ /**
18
+ * Reaper / organisational-mapping tag carrying the real customer application id
19
+ * (distinct from the `fjall-dev-<app>` CDK App identity that drives the
20
+ * substrate's stack names). NOT part of the IAM fence contract.
21
+ */
22
+ export declare const DEV_APP_TAG_KEY: "fjall:app";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Tags applied by the internal `DevSubstrate` construct.
3
+ *
4
+ * `fjall:dev = "true"` is IAM-load-bearing, not cosmetic. The shipped dev-tier
5
+ * deploy fence in `@fjall/generator` (`src/schemas/devRolePolicies.ts`) gates its
6
+ * slot-ops grants on `aws:ResourceTag/fjall:dev = "true"` and
7
+ * `aws:RequestTag/fjall:dev = "true"`. The key and value here MUST stay
8
+ * byte-identical to those IAM condition keys — any drift silently unfences the
9
+ * substrate (the runtime-name-must-match-construct coupling class).
10
+ *
11
+ * A util-level single source shared with the policy is a candidate follow-up; it
12
+ * is deliberately NOT folded here, to avoid editing the parity-locked security
13
+ * policy from a construct-scaffold change.
14
+ */
15
+ export const DEV_TAG_KEY = "fjall:dev";
16
+ export const DEV_TAG_VALUE = "true";
17
+ /**
18
+ * Reaper / organisational-mapping tag carrying the real customer application id
19
+ * (distinct from the `fjall-dev-<app>` CDK App identity that drives the
20
+ * substrate's stack names). NOT part of the IAM fence contract.
21
+ */
22
+ export const DEV_APP_TAG_KEY = "fjall:app";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "2.31.1",
3
+ "version": "2.32.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "clean": "rm -rf ./dist",
35
35
  "clean:node": "rm -rf ./node_modules",
36
36
  "build:cert-gen-lambda": "node lib/lambda-assets/cert-generator/src/build.mjs",
37
- "build": "npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs",
37
+ "build": "npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs && cp lib/patterns/aws/devSubstrate.waker.source.cjs dist/lib/patterns/aws/devSubstrate.waker.source.cjs",
38
38
  "prepack": "node ../../scripts/check-dist-freshness.mjs",
39
39
  "watch": "tsc -w",
40
40
  "watch:only": "tsc -w",
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@aws-sdk/client-organizations": "^3.1038.0",
70
- "@fjall/generator": "^2.31.1",
71
- "@fjall/util": "^2.31.1",
70
+ "@fjall/generator": "^2.32.0",
71
+ "@fjall/util": "^2.32.0",
72
72
  "constructs": "^10.6.0"
73
73
  },
74
74
  "overrides": {
@@ -82,5 +82,5 @@
82
82
  "engines": {
83
83
  "node": ">=18.0.0"
84
84
  },
85
- "gitHead": "a48cfaf6e050ce1736837802dfcaf517ba39f199"
85
+ "gitHead": "876a79a9ad0031a5919019c86867c28c1ab9dc92"
86
86
  }