@fjall/components-infrastructure 3.4.1 → 3.5.2

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.
@@ -23,15 +23,24 @@ export declare class Buildkite extends Construct {
23
23
  readonly secretsBucketName: string;
24
24
  readonly autoScalingGroupName: string;
25
25
  constructor(scope: Construct, id: string, props: BuildkiteConstructProps);
26
+ /**
27
+ * Derive an agent-cluster secret path from the app namespace — the same
28
+ * `buildParameterPath` derivation `fjall secrets` and the ECS pattern use,
29
+ * so derived parameters stay manageable by the shared tooling. Explicit
30
+ * props bypass this entirely; it throws only when derivation is the last
31
+ * resort and no valid `applicationId` exists to derive from.
32
+ */
33
+ private deriveAgentSecretPath;
26
34
  /**
27
35
  * Ship the per-job `env` hook into the managed secrets bucket. The
28
36
  * s3-secrets-hooks plugin sources this file at the start of EVERY job, so
29
- * FJALL_API_KEY is read fresh from SSM per job rotation takes effect on
30
- * the next job with no instance replacement (design § D4(iii)). The script
31
- * is configuration, not a secret: shipping it through a CDK asset is fine;
32
- * the secret VALUE only ever moves SSM instance at job runtime.
37
+ * FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM
38
+ * per job — rotation takes effect on the next job with no instance
39
+ * replacement (design § D4(iii)). The script is configuration, not a
40
+ * secret: shipping it through a CDK asset is fine; secret VALUES only ever
41
+ * move SSM → instance at job runtime.
33
42
  */
34
- private addFjallApiKeyEnvHook;
43
+ private addAgentSecretsEnvHook;
35
44
  }
36
45
  /**
37
46
  * Object key the s3-secrets-hooks plugin sources at the start of every job —
@@ -39,8 +48,44 @@ export declare class Buildkite extends Construct {
39
48
  */
40
49
  export declare const FJALL_ENV_HOOK_OBJECT_KEY = "env";
41
50
  /**
42
- * The per-job env hook's script content (design § D4(iii)): FJALL_API_KEY is
43
- * read fresh from SSM per job, so rotation takes effect on the next job with
44
- * no instance replacement. Pure — the unit test pins the exact content.
51
+ * Cluster segment of derived agent secret paths
52
+ * (`/<applicationId>/agents/<name>`) the namespace the live estate already
53
+ * uses for its explicitly-provisioned parameters.
54
+ */
55
+ export declare const BUILDKITE_SECRETS_CLUSTER = "agents";
56
+ /** Derived secret name for the cluster-scoped Buildkite agent token. */
57
+ export declare const AGENT_TOKEN_SECRET_NAME = "agent-token";
58
+ /** Derived secret name for the FJALL_API_KEY env-hook parameter. */
59
+ export declare const FJALL_API_KEY_SECRET_NAME = "fjall-api-key";
60
+ /** Inputs to {@link buildAgentSecretsEnvHookScript}; omitted members skip their block. */
61
+ export interface AgentSecretsEnvHookOptions {
62
+ /** Resolved FJALL_API_KEY parameter path (explicit prop or derived). */
63
+ readonly fjallApiKeyParameterName?: string;
64
+ /**
65
+ * Namespace root for per-pipeline job secrets (`/<applicationId>/agents`);
66
+ * the hook lists `<prefix>/<BUILDKITE_PIPELINE_SLUG>` per job. Jobs read
67
+ * the whole `/<app>/agents/*` subtree via the paired IAM grant — identical
68
+ * flatness to the previous whole-bucket S3 read; isolation stays per-fleet.
69
+ */
70
+ readonly pipelineSecretsPrefix?: string;
71
+ readonly region: string;
72
+ }
73
+ /**
74
+ * The per-job env hook's script content (design § D4(iii) + Phase B):
75
+ * FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM per
76
+ * job, so rotation takes effect on the next job with no instance replacement.
77
+ * Pure — the unit tests byte-pin both emitted variants and `sh -n` them.
78
+ *
79
+ * The script is eval'd by the s3secrets plugin under bash
80
+ * `set -Eeuo pipefail` + an ERR trap that exits 53 (fails the job) +
81
+ * `set -o allexport` (every assignment auto-exports) + `nounset` — pinned in
82
+ * `lib/__tests__/fixtures/buildkite/upstream-hooks-v6.68.1/`. Hence: every
83
+ * fallible command is guarded, every env read uses `${VAR:-}`, every temp
84
+ * variable is unset after use, and the script never exits. `private_ssh_key`
85
+ * loads into an ephemeral ssh-agent (stdin → agent memory, never disk or
86
+ * argv); the upstream pre-exit hook kills whatever `SSH_AGENT_PID` is
87
+ * exported, so our agent's lifecycle is owned upstream. Every fetched value
88
+ * is registered with the agent redactor (stdin form) before use — the
89
+ * upstream helper cannot see values we fetch at eval time.
45
90
  */
46
- export declare function buildFjallApiKeyEnvHookScript(parameterName: string, region: string): string;
91
+ export declare function buildAgentSecretsEnvHookScript(options: AgentSecretsEnvHookOptions): string;
@@ -1,4 +1,5 @@
1
- import { CfnOutput, Duration, RemovalPolicy, Stack, Token } from "aws-cdk-lib";
1
+ import { Annotations, CfnOutput, Duration, RemovalPolicy, Stack, Token } from "aws-cdk-lib";
2
+ import { SSM_COMPONENT_PATTERN, buildParameterPath } from "@fjall/util";
2
3
  import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
3
4
  import { InstanceArchitecture, InstanceType, MachineImage, SubnetType } from "aws-cdk-lib/aws-ec2";
4
5
  import { PolicyStatement } from "aws-cdk-lib/aws-iam";
@@ -38,6 +39,37 @@ export class Buildkite extends Construct {
38
39
  super(scope, id);
39
40
  const { vpc, ...plainProps } = props;
40
41
  const config = validateBuildkiteProps(plainProps);
42
+ // Deliberate asymmetry: the agent token is load-bearing (boot + scaler),
43
+ // so underivable throws; the api-key hook and per-pipeline job secrets
44
+ // are capabilities, so underivable — including a pattern-invalid
45
+ // applicationId, which pre-derivation callers could always pass —
46
+ // degrades to "not shipped" with a warning.
47
+ const agentTokenParameterName = config.agentTokenSsmParameterName ??
48
+ this.deriveAgentSecretPath(config.applicationId, AGENT_TOKEN_SECRET_NAME);
49
+ const derivableApplicationId = config.applicationId !== undefined &&
50
+ config.applicationId !== "" &&
51
+ SSM_COMPONENT_PATTERN.test(config.applicationId)
52
+ ? config.applicationId
53
+ : undefined;
54
+ const fjallApiKeyParameterName = config.fjallApiKeySsmParameterName ??
55
+ (derivableApplicationId !== undefined
56
+ ? this.deriveAgentSecretPath(derivableApplicationId, FJALL_API_KEY_SECRET_NAME)
57
+ : undefined);
58
+ const pipelineSecretsPrefix = derivableApplicationId !== undefined
59
+ ? buildParameterPath({
60
+ app: derivableApplicationId,
61
+ cluster: BUILDKITE_SECRETS_CLUSTER
62
+ })
63
+ : undefined;
64
+ if (derivableApplicationId === undefined &&
65
+ config.applicationId !== undefined &&
66
+ config.applicationId !== "") {
67
+ Annotations.of(this).addWarningV2("fjall:buildkite:fjall-api-key-hook-skipped", `applicationId '${config.applicationId}' is not a valid SSM path ` +
68
+ `component, so namespace-derived agent secrets are disabled: ` +
69
+ `per-pipeline job secrets will not load, and the FJALL_API_KEY env ` +
70
+ `hook is not shipped unless fjallApiKeySsmParameterName is set ` +
71
+ `explicitly. Rename the app or set the parameter-name props.`);
72
+ }
41
73
  const stack = Stack.of(this);
42
74
  if (Token.isUnresolved(stack.region)) {
43
75
  throw new Error("Buildkite requires a concrete env region at synth (the pinned AMI " +
@@ -61,17 +93,27 @@ export class Buildkite extends Construct {
61
93
  // and accidental overwrite; the artifact bucket is scratch output and
62
94
  // keeps the wrapper's env-aware default (DESTROY + pre-empty off prod).
63
95
  const managedSecretsBucket = new S3Bucket(this, `${id}ManagedSecretsBucket`, { versioned: true, removalPolicy: RemovalPolicy.RETAIN });
64
- if (config.fjallApiKeySsmParameterName !== undefined) {
65
- this.addFjallApiKeyEnvHook(id, managedSecretsBucket, config.fjallApiKeySsmParameterName, region);
96
+ if (fjallApiKeyParameterName !== undefined ||
97
+ pipelineSecretsPrefix !== undefined) {
98
+ this.addAgentSecretsEnvHook(id, managedSecretsBucket, {
99
+ ...(fjallApiKeyParameterName !== undefined && {
100
+ fjallApiKeyParameterName
101
+ }),
102
+ ...(pipelineSecretsPrefix !== undefined && { pipelineSecretsPrefix }),
103
+ region
104
+ });
66
105
  }
67
106
  const parameterArn = (name) => `arn:${stack.partition}:ssm:${region}:${stack.account}:parameter${name}`;
68
107
  const agentRole = buildAgentRole(this, `${id}AgentRole`, {
69
- agentTokenParameterArn: parameterArn(config.agentTokenSsmParameterName),
108
+ agentTokenParameterArn: parameterArn(agentTokenParameterName),
70
109
  ...(config.agentTokenKmsKeyArn !== undefined && {
71
110
  agentTokenKmsKeyArn: config.agentTokenKmsKeyArn
72
111
  }),
73
- ...(config.fjallApiKeySsmParameterName !== undefined && {
74
- fjallApiKeyParameterArn: parameterArn(config.fjallApiKeySsmParameterName)
112
+ ...(fjallApiKeyParameterName !== undefined && {
113
+ fjallApiKeyParameterArn: parameterArn(fjallApiKeyParameterName)
114
+ }),
115
+ ...(pipelineSecretsPrefix !== undefined && {
116
+ pipelineSecretsPrefixArn: parameterArn(pipelineSecretsPrefix)
75
117
  }),
76
118
  secretsBucketArn: managedSecretsBucket.bucketArn,
77
119
  artifactBucketArn: artifactBucket.bucketArn,
@@ -82,7 +124,8 @@ export class Buildkite extends Construct {
82
124
  stackName: stack.stackName,
83
125
  region,
84
126
  secretsBucketName: managedSecretsBucket.bucketName,
85
- artifactBucketName: artifactBucket.bucketName
127
+ artifactBucketName: artifactBucket.bucketName,
128
+ agentTokenParameterName
86
129
  });
87
130
  // Deliberate deviation from upstream's InstanceScaleInProtection
88
131
  // (design § D7): the wrapper's `newInstancesProtectedFromScaleIn: false`
@@ -146,7 +189,7 @@ export class Buildkite extends Construct {
146
189
  applicationId: resolveScalerSarApplicationArn(architecture),
147
190
  semanticVersion: BUILDKITE_STACK_PINS.scalerVersion,
148
191
  parameters: {
149
- BuildkiteAgentTokenParameter: config.agentTokenSsmParameterName,
192
+ BuildkiteAgentTokenParameter: agentTokenParameterName,
150
193
  ...(config.agentTokenKmsKeyArn !== undefined && {
151
194
  BuildkiteAgentTokenParameterStoreKMSKey: config.agentTokenKmsKeyArn
152
195
  }),
@@ -201,18 +244,43 @@ export class Buildkite extends Construct {
201
244
  value: artifactBucket.bucketName
202
245
  });
203
246
  }
247
+ /**
248
+ * Derive an agent-cluster secret path from the app namespace — the same
249
+ * `buildParameterPath` derivation `fjall secrets` and the ECS pattern use,
250
+ * so derived parameters stay manageable by the shared tooling. Explicit
251
+ * props bypass this entirely; it throws only when derivation is the last
252
+ * resort and no valid `applicationId` exists to derive from.
253
+ */
254
+ deriveAgentSecretPath(applicationId, secretName) {
255
+ if (applicationId === undefined || applicationId === "") {
256
+ throw new Error(`Buildkite: no explicit SSM parameter name was set for '${secretName}' ` +
257
+ `and no applicationId is available to derive ` +
258
+ `'/<applicationId>/${BUILDKITE_SECRETS_CLUSTER}/${secretName}' from. ` +
259
+ `Construct via App.addBuildkite (which injects applicationId) or set ` +
260
+ `the parameter-name prop explicitly.`);
261
+ }
262
+ if (!SSM_COMPONENT_PATTERN.test(applicationId)) {
263
+ throw new Error(`Buildkite: cannot derive the '${secretName}' SSM parameter path — ` +
264
+ `applicationId '${applicationId}' is not a valid SSM path component ` +
265
+ `(must start with a letter; letters, numbers, periods, hyphens, ` +
266
+ `underscores only). Rename the app or set the parameter-name prop ` +
267
+ `explicitly.`);
268
+ }
269
+ return buildParameterPath({ app: applicationId, cluster: BUILDKITE_SECRETS_CLUSTER }, secretName);
270
+ }
204
271
  /**
205
272
  * Ship the per-job `env` hook into the managed secrets bucket. The
206
273
  * s3-secrets-hooks plugin sources this file at the start of EVERY job, so
207
- * FJALL_API_KEY is read fresh from SSM per job rotation takes effect on
208
- * the next job with no instance replacement (design § D4(iii)). The script
209
- * is configuration, not a secret: shipping it through a CDK asset is fine;
210
- * the secret VALUE only ever moves SSM instance at job runtime.
274
+ * FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM
275
+ * per job — rotation takes effect on the next job with no instance
276
+ * replacement (design § D4(iii)). The script is configuration, not a
277
+ * secret: shipping it through a CDK asset is fine; secret VALUES only ever
278
+ * move SSM → instance at job runtime.
211
279
  */
212
- addFjallApiKeyEnvHook(id, secretsBucket, parameterName, region) {
280
+ addAgentSecretsEnvHook(id, secretsBucket, options) {
213
281
  new BucketDeployment(this, `${id}EnvHookDeployment`, {
214
282
  sources: [
215
- Source.data(FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript(parameterName, region))
283
+ Source.data(FJALL_ENV_HOOK_OBJECT_KEY, buildAgentSecretsEnvHookScript(options))
216
284
  ],
217
285
  destinationBucket: secretsBucket,
218
286
  // The secrets bucket also holds out-of-band objects the deployment
@@ -230,17 +298,57 @@ export class Buildkite extends Construct {
230
298
  */
231
299
  export const FJALL_ENV_HOOK_OBJECT_KEY = "env";
232
300
  /**
233
- * The per-job env hook's script content (design § D4(iii)): FJALL_API_KEY is
234
- * read fresh from SSM per job, so rotation takes effect on the next job with
235
- * no instance replacement. Pure — the unit test pins the exact content.
301
+ * Cluster segment of derived agent secret paths
302
+ * (`/<applicationId>/agents/<name>`) the namespace the live estate already
303
+ * uses for its explicitly-provisioned parameters.
304
+ */
305
+ export const BUILDKITE_SECRETS_CLUSTER = "agents";
306
+ /** Derived secret name for the cluster-scoped Buildkite agent token. */
307
+ export const AGENT_TOKEN_SECRET_NAME = "agent-token";
308
+ /** Derived secret name for the FJALL_API_KEY env-hook parameter. */
309
+ export const FJALL_API_KEY_SECRET_NAME = "fjall-api-key";
310
+ /**
311
+ * The per-job env hook's script content (design § D4(iii) + Phase B):
312
+ * FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM per
313
+ * job, so rotation takes effect on the next job with no instance replacement.
314
+ * Pure — the unit tests byte-pin both emitted variants and `sh -n` them.
315
+ *
316
+ * The script is eval'd by the s3secrets plugin under bash
317
+ * `set -Eeuo pipefail` + an ERR trap that exits 53 (fails the job) +
318
+ * `set -o allexport` (every assignment auto-exports) + `nounset` — pinned in
319
+ * `lib/__tests__/fixtures/buildkite/upstream-hooks-v6.68.1/`. Hence: every
320
+ * fallible command is guarded, every env read uses `${VAR:-}`, every temp
321
+ * variable is unset after use, and the script never exits. `private_ssh_key`
322
+ * loads into an ephemeral ssh-agent (stdin → agent memory, never disk or
323
+ * argv); the upstream pre-exit hook kills whatever `SSH_AGENT_PID` is
324
+ * exported, so our agent's lifecycle is owned upstream. Every fetched value
325
+ * is registered with the agent redactor (stdin form) before use — the
326
+ * upstream helper cannot see values we fetch at eval time.
236
327
  */
237
- export function buildFjallApiKeyEnvHookScript(parameterName, region) {
238
- return [
239
- `FJALL_API_KEY="$(aws ssm get-parameter --name '${parameterName}' --with-decryption --query Parameter.Value --output text --region '${region}')"`,
240
- "export FJALL_API_KEY",
241
- ""
242
- ].join("\n");
328
+ export function buildAgentSecretsEnvHookScript(options) {
329
+ const { fjallApiKeyParameterName, pipelineSecretsPrefix, region } = options;
330
+ const lines = ["set +x", "set +v"];
331
+ if (fjallApiKeyParameterName !== undefined) {
332
+ lines.push(`FJALL_API_KEY="$(aws ssm get-parameter --name '${fjallApiKeyParameterName}' --with-decryption --query Parameter.Value --output text --region '${region}' 2>/dev/null)" || true`, 'if [ -n "${FJALL_API_KEY:-}" ]; then', ` printf '%s' "$FJALL_API_KEY" | buildkite-agent redactor add >/dev/null 2>&1 || true`, " export FJALL_API_KEY", "else", " unset FJALL_API_KEY", ` echo 'fjall-secrets: could not fetch ${fjallApiKeyParameterName}' >&2`, "fi");
333
+ }
334
+ if (pipelineSecretsPrefix !== undefined) {
335
+ lines.push('if [ -n "${BUILDKITE_PIPELINE_SLUG:-}" ]; then', ` _fjall_names="$(aws ssm get-parameters-by-path --path "${pipelineSecretsPrefix}/\${BUILDKITE_PIPELINE_SLUG:-}" --query 'Parameters[].Name' --output text --region '${region}' 2>/dev/null)" || true`, " for _fjall_param in ${_fjall_names:-}; do", ' _fjall_name="${_fjall_param##*/}"', ` _fjall_value="$(aws ssm get-parameter --name "$_fjall_param" --with-decryption --query Parameter.Value --output text --region '${region}' 2>/dev/null)" || true`, ' if [ -z "${_fjall_value:-}" ]; then', ' echo "fjall-secrets: could not fetch $_fjall_param" >&2', " unset _fjall_value", " continue", " fi", ` printf '%s' "$_fjall_value" | buildkite-agent redactor add >/dev/null 2>&1 || true`, ` if [ "$_fjall_name" = 'private_ssh_key' ]; then`, ` _fjall_agent_out="$(ssh-agent -s 2>/dev/null)" || true`, ' if [ -n "${_fjall_agent_out:-}" ]; then', ' eval "$_fjall_agent_out" >/dev/null 2>&1 || true', ` printf '%s\\n' "$_fjall_value" | ssh-add - >/dev/null 2>&1 || echo 'fjall-secrets: ssh-add failed for private_ssh_key' >&2`, " else", ` echo 'fjall-secrets: ssh-agent unavailable - private_ssh_key not loaded' >&2`, " fi", " unset _fjall_agent_out", ` elif printf '%s' "$_fjall_name" | grep -Eq '${JOB_ENV_NAME_ALLOW_REGEX}' && ! printf '%s' "$_fjall_name" | grep -Eq '${JOB_ENV_NAME_DENY_REGEX}'; then`, ` export "$_fjall_name=$_fjall_value" 2>/dev/null || echo "fjall-secrets: could not export $_fjall_name" >&2`, " else", ' echo "fjall-secrets: skipping non-conforming secret name $_fjall_name" >&2', " fi", " unset _fjall_value", " done", " unset _fjall_names", " unset _fjall_param", " unset _fjall_name", "fi");
336
+ }
337
+ lines.push("");
338
+ return lines.join("\n");
243
339
  }
340
+ /**
341
+ * Job-env eligibility for per-pipeline secret names: the allow shape is a
342
+ * portable shell identifier; the deny list removes the bash-readonly names
343
+ * (`export UID=…` fails "readonly variable" — under the upstream ERR trap
344
+ * that would exit-53 EVERY job on the pipeline until the parameter is
345
+ * deleted), the ssh-agent lifecycle vars the D-B3 flow owns, the
346
+ * loader-injection vector vars, and the `BUILDKITE_`/`AWS_` prefixes.
347
+ * `private_ssh_key` is handled before this filter; denied/non-conforming
348
+ * names are skipped with an identifier-only warning.
349
+ */
350
+ const JOB_ENV_NAME_ALLOW_REGEX = "^[A-Za-z_][A-Za-z0-9_]*$";
351
+ const JOB_ENV_NAME_DENY_REGEX = "^(PATH|HOME|IFS|LD_PRELOAD|LD_LIBRARY_PATH|BASH_ENV|SSH_AUTH_SOCK|SSH_AGENT_PID|UID|EUID|PPID|SHELLOPTS|BASHOPTS|BASH_VERSINFO)$|^(BUILDKITE_|AWS_)";
244
352
  function deriveCpuArchitecture(instanceTypeIdentifier) {
245
353
  const architecture = new InstanceType(instanceTypeIdentifier).architecture;
246
354
  return architecture === InstanceArchitecture.ARM_64 ? "arm64" : "amd64";
@@ -11,11 +11,18 @@ import { Role } from "../../../resources/aws/iam/index.js";
11
11
  * subset of this list, so an upstream bump (or a future edit) that grows
12
12
  * permissions fails loudly instead of shipping silently.
13
13
  */
14
- export declare const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST: readonly ["ssm:GetParameter", "kms:Decrypt", "s3:GetObject", "s3:ListBucket", "s3:GetObjectVersion", "s3:PutObject", "s3:PutObjectAcl", "s3:PutObjectVersionAcl", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:PutRetentionPolicy", "ssm:DescribeInstanceProperties", "ssm:ListAssociations", "ssm:PutInventory", "ssm:UpdateInstanceInformation", "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel", "ec2messages:AcknowledgeMessage", "ec2messages:DeleteMessage", "ec2messages:FailMessage", "ec2messages:GetEndpoint", "ec2messages:GetMessages", "ec2messages:SendReply", "autoscaling:DescribeAutoScalingInstances", "autoscaling:SetInstanceHealth", "autoscaling:TerminateInstanceInAutoScalingGroup", "cloudwatch:PutMetricData", "cloudformation:DescribeStackResource", "ec2:DescribeTags"];
14
+ export declare const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST: readonly ["ssm:GetParameter", "ssm:GetParametersByPath", "kms:Decrypt", "s3:GetObject", "s3:ListBucket", "s3:GetObjectVersion", "s3:PutObject", "s3:PutObjectAcl", "s3:PutObjectVersionAcl", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:PutRetentionPolicy", "ssm:DescribeInstanceProperties", "ssm:ListAssociations", "ssm:PutInventory", "ssm:UpdateInstanceInformation", "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel", "ec2messages:AcknowledgeMessage", "ec2messages:DeleteMessage", "ec2messages:FailMessage", "ec2messages:GetEndpoint", "ec2messages:GetMessages", "ec2messages:SendReply", "autoscaling:DescribeAutoScalingInstances", "autoscaling:SetInstanceHealth", "autoscaling:TerminateInstanceInAutoScalingGroup", "cloudwatch:PutMetricData", "cloudformation:DescribeStackResource", "ec2:DescribeTags"];
15
15
  export interface BuildkiteAgentRoleParams {
16
16
  readonly agentTokenParameterArn: string;
17
17
  readonly agentTokenKmsKeyArn?: string;
18
18
  readonly fjallApiKeyParameterArn?: string;
19
+ /**
20
+ * `arn:…:parameter/<app>/agents` — per-pipeline job-secrets subtree root;
21
+ * grants list + read on `<root>/*`. Jobs can read the whole subtree
22
+ * (identical flatness to the whole-bucket S3 read this replaces);
23
+ * isolation stays per-fleet.
24
+ */
25
+ readonly pipelineSecretsPrefixArn?: string;
19
26
  readonly secretsBucketArn: string;
20
27
  readonly artifactBucketArn: string;
21
28
  /** Own-stack ARN — `cloudformation:DescribeStackResource` scope. */
@@ -13,6 +13,9 @@ import { Role } from "../../../resources/aws/iam/index.js";
13
13
  */
14
14
  export const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST = [
15
15
  "ssm:GetParameter",
16
+ // Read-only, scoped to the /<app>/agents/* subtree (per-pipeline job
17
+ // secrets) — never account-wide.
18
+ "ssm:GetParametersByPath",
16
19
  "kms:Decrypt",
17
20
  "s3:GetObject",
18
21
  "s3:ListBucket",
@@ -77,6 +80,14 @@ function buildAgentPolicyDocuments(params) {
77
80
  : [])
78
81
  ]
79
82
  }),
83
+ ...(params.pipelineSecretsPrefixArn !== undefined
84
+ ? [
85
+ new PolicyStatement({
86
+ actions: ["ssm:GetParameter", "ssm:GetParametersByPath"],
87
+ resources: [`${params.pipelineSecretsPrefixArn}/*`]
88
+ })
89
+ ]
90
+ : []),
80
91
  ...(params.agentTokenKmsKeyArn !== undefined
81
92
  ? [
82
93
  new PolicyStatement({
@@ -11,6 +11,13 @@ export interface BuildkiteUserDataContext {
11
11
  readonly region: string;
12
12
  readonly secretsBucketName: string;
13
13
  readonly artifactBucketName: string;
14
+ /**
15
+ * RESOLVED agent-token parameter path (explicit prop or namespace-derived)
16
+ * — never `props.agentTokenSsmParameterName`, which is optional since the
17
+ * derivation landed and would render the literal string "undefined" into
18
+ * BUILDKITE_AGENT_TOKEN_PATH through the template literal.
19
+ */
20
+ readonly agentTokenParameterName: string;
14
21
  }
15
22
  /**
16
23
  * The pinned AMI's boot scripts run under `set -Eeuo pipefail` and
@@ -42,7 +49,7 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
42
49
  * `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
43
50
  * upstream order (so a fixture-bump diff reads line-by-line). Keys with no
44
51
  * schema knob carry the upstream v6.68.1 parameter default verbatim, with
45
- * four deliberate deviations:
52
+ * five deliberate deviations:
46
53
  *
47
54
  * - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
48
55
  * disabled), where S3 rejects every canned ACL EXCEPT
@@ -54,6 +61,11 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
54
61
  * is out of scope for the Phase-1 deploy fleet; a future signing knob must
55
62
  * land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
56
63
  * knob shipped without them — a dead toggle that broke agents when set).
64
+ * - `BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING`: hard-pinned
65
+ * `true` (upstream default `false`). Since the Phase-B migration the git
66
+ * deploy key lives in SSM and is loaded by our env hook — the plugin's S3
67
+ * `private_ssh_key` object is intentionally absent, so its per-job
68
+ * not-found warning is pure noise.
57
69
  * - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
58
70
  * `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
59
71
  * bounded retention everywhere: each booting instance applies
@@ -149,7 +149,7 @@ export function buildBuildkiteDockerCommands(props) {
149
149
  * `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
150
150
  * upstream order (so a fixture-bump diff reads line-by-line). Keys with no
151
151
  * schema knob carry the upstream v6.68.1 parameter default verbatim, with
152
- * four deliberate deviations:
152
+ * five deliberate deviations:
153
153
  *
154
154
  * - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
155
155
  * disabled), where S3 rejects every canned ACL EXCEPT
@@ -161,6 +161,11 @@ export function buildBuildkiteDockerCommands(props) {
161
161
  * is out of scope for the Phase-1 deploy fleet; a future signing knob must
162
162
  * land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
163
163
  * knob shipped without them — a dead toggle that broke agents when set).
164
+ * - `BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING`: hard-pinned
165
+ * `true` (upstream default `false`). Since the Phase-B migration the git
166
+ * deploy key lives in SSM and is loaded by our env hook — the plugin's S3
167
+ * `private_ssh_key` object is intentionally absent, so its per-job
168
+ * not-found warning is pure noise.
164
169
  * - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
165
170
  * `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
166
171
  * bounded retention everywhere: each booting instance applies
@@ -181,11 +186,11 @@ export function buildBuildkiteInstallCommands(props, context) {
181
186
  `BUILDKITE_SCALE_IN_IDLE_PERIOD='${props.scaleInIdlePeriodSeconds}' \\`,
182
187
  `BUILDKITE_SECRETS_BUCKET='${context.secretsBucketName}' \\`,
183
188
  `BUILDKITE_SECRETS_BUCKET_REGION='${context.region}' \\`,
184
- "BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING='false' \\",
189
+ "BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING='true' \\",
185
190
  `BUILDKITE_ARTIFACTS_BUCKET='${context.artifactBucketName}' \\`,
186
191
  `BUILDKITE_S3_DEFAULT_REGION='${context.region}' \\`,
187
192
  "BUILDKITE_S3_ACL='bucket-owner-full-control' \\",
188
- `BUILDKITE_AGENT_TOKEN_PATH='${props.agentTokenSsmParameterName}' \\`,
193
+ `BUILDKITE_AGENT_TOKEN_PATH='${context.agentTokenParameterName}' \\`,
189
194
  `BUILDKITE_AGENTS_PER_INSTANCE='${props.agentsPerInstance}' \\`,
190
195
  "BUILDKITE_AGENT_ENDPOINT='https://agent-edge.buildkite.com/v3' \\",
191
196
  `BUILDKITE_AGENT_TAGS='${props.buildkiteAgentTags}' \\`,
@@ -4,7 +4,7 @@
4
4
  * Construct instantiated via `App.addBuildkite()`; design:
5
5
  * `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`.
6
6
  */
7
- export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript, type BuildkiteConstructProps } from "./buildkite/buildkite.js";
7
+ export { AGENT_TOKEN_SECRET_NAME, BUILDKITE_SECRETS_CLUSTER, Buildkite, FJALL_API_KEY_SECRET_NAME, FJALL_ENV_HOOK_OBJECT_KEY, buildAgentSecretsEnvHookScript, type AgentSecretsEnvHookOptions, type BuildkiteConstructProps } from "./buildkite/buildkite.js";
8
8
  export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn, type BuildkiteCpuArchitecture } from "./buildkite/pins.js";
9
9
  export { BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput } from "./buildkite/schema.js";
10
10
  export { BuildkiteFactory, type BuildkiteFactoryFn } from "./buildkite/factory.js";
@@ -4,7 +4,7 @@
4
4
  * Construct instantiated via `App.addBuildkite()`; design:
5
5
  * `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`.
6
6
  */
7
- export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript } from "./buildkite/buildkite.js";
7
+ export { AGENT_TOKEN_SECRET_NAME, BUILDKITE_SECRETS_CLUSTER, Buildkite, FJALL_API_KEY_SECRET_NAME, FJALL_ENV_HOOK_OBJECT_KEY, buildAgentSecretsEnvHookScript } from "./buildkite/buildkite.js";
8
8
  export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./buildkite/pins.js";
9
9
  export { BuildkitePropsSchema, validateBuildkiteProps } from "./buildkite/schema.js";
10
10
  export { BuildkiteFactory } from "./buildkite/factory.js";
@@ -69,6 +69,12 @@ export interface AuroraDatabaseProps extends BaseDatabaseProps {
69
69
  clusterIdentifier?: string;
70
70
  /** Physical name of the generated master-credentials secret. */
71
71
  credentialsSecretName?: string;
72
+ /**
73
+ * KMS alias for the CMK encrypting the master-credentials secret. Must be
74
+ * app-scoped when the same `databaseName` deploys once per app in a shared
75
+ * account (aliases are account+region-unique).
76
+ */
77
+ credentialsKmsAliasName?: string;
72
78
  /** Serverless-v2 floor in ACU. 0 enables scale-to-zero (auto-pause). */
73
79
  serverlessV2MinCapacity?: number;
74
80
  /** Serverless-v2 ceiling in ACU. */
@@ -16,7 +16,7 @@ import { Duration } from "aws-cdk-lib";
16
16
  export { isAwsManagedKey, isCMKRequested, AWS_MANAGED, USE_CMK } from "../../utils/databaseTypes.js";
17
17
  export const DATABASE_ENGINE_CONFIG = Object.freeze({
18
18
  postgresql: {
19
- aurora: { version: "16.6", majorVersion: "16" },
19
+ aurora: { version: "16.10", majorVersion: "16" },
20
20
  instance: { version: "17.5", majorVersion: "17" },
21
21
  defaultUsername: "postgres",
22
22
  sslParameters: { "rds.force_ssl": "1" }
@@ -372,6 +372,7 @@ export class RelationalDatabase extends Construct {
372
372
  engineConfig,
373
373
  clusterIdentifier: props.clusterIdentifier,
374
374
  credentialsSecretName: props.credentialsSecretName,
375
+ credentialsKmsAliasName: props.credentialsKmsAliasName,
375
376
  serverlessV2MinCapacity: props.serverlessV2MinCapacity,
376
377
  serverlessV2MaxCapacity: props.serverlessV2MaxCapacity,
377
378
  serverlessV2AutoPauseDuration: props.serverlessV2AutoPauseDuration,
@@ -161,6 +161,8 @@ export class DevSubstrate extends Construct {
161
161
  databaseName: DEV_SUBSTRATE_DATABASE_NAME,
162
162
  clusterIdentifier: devFenceName(props.appKebab),
163
163
  credentialsSecretName: `${devFenceName(props.appKebab)}-master`,
164
+ // Default alias derives from the fixed databaseName — collides account-wide.
165
+ credentialsKmsAliasName: `cmk/${devFenceName(props.appKebab)}-master`,
164
166
  engineVersion: props.engineVersion ?? DEV_AURORA_DEFAULTS.ENGINE_VERSION,
165
167
  serverlessV2MinCapacity: DEV_AURORA_DEFAULTS.SERVERLESS_V2_MIN_CAPACITY,
166
168
  serverlessV2MaxCapacity: DEV_AURORA_DEFAULTS.SERVERLESS_V2_MAX_CAPACITY,
@@ -205,8 +207,14 @@ export class DevSubstrate extends Construct {
205
207
  // createBaseExecutionRole DELIBERATELY omits logs (auto-granted only on the
206
208
  // ECS-pattern path via AwsLogDriver.bind()). Slot task-defs reference this
207
209
  // shared role by ARN, so that auto-grant never fires — add the log-write grant
208
- // explicitly.
209
- const slotExecutionRole = createBaseExecutionRole(this, DEV_SUBSTRATE_SLOT_EXEC_ROLE_ID);
210
+ // explicitly. Path + name pin the role inside the /fjall/dev/ fence: the dev
211
+ // deploy role's only iam:PassRole grant is on role/fjall/dev/*, so the CDK
212
+ // defaults (path "/", CFN-generated name) would AccessDeny slot task-def
213
+ // registration.
214
+ const slotExecutionRole = createBaseExecutionRole(this, DEV_SUBSTRATE_SLOT_EXEC_ROLE_ID, {
215
+ path: "/fjall/dev/",
216
+ roleName: `${devFenceName(props.appKebab)}-slot-exec`
217
+ });
210
218
  const { partition, region, account } = Stack.of(this);
211
219
  slotExecutionRole.addToPolicy(new PolicyStatement({
212
220
  effect: Effect.ALLOW,
@@ -376,9 +384,11 @@ export class DevSubstrate extends Construct {
376
384
  * the hand-created G2 slot service consumes these to place tasks, register a
377
385
  * host-header rule, inject DB creds and point a sleeping slot at the waker.
378
386
  *
379
- * No KMS-alias export: the substrate's Aurora + waker use the AWS-managed default
380
- * key (design VF4 the waker's kms:Decrypt is ViaService-scoped precisely
381
- * because there is no per-app CMK), so there is no alias to export.
387
+ * No KMS-alias export: cluster storage and SSM SecureStrings ride AWS-managed
388
+ * default keys, and while the master-credentials secret IS CMK-encrypted (the
389
+ * Secret wrapper mints `cmk/fjall-dev-<app>-master`), no consumer addresses
390
+ * that key by alias — the provisioner's and waker's kms:Decrypt are
391
+ * ViaService-scoped (Secrets Manager / SSM), so there is no alias to export.
382
392
  *
383
393
  * The Aurora slot-SG ingress (A2.5) makes the Database stack depend on the
384
394
  * Compute stack. So the two DB exports are scoped under `databaseScope` (the
@@ -16,9 +16,16 @@ import type { EcsServiceProps } from "./ecsTypes.js";
16
16
  * SHARED or IMPORTED role those auto-grants cannot reach — e.g. the dev
17
17
  * substrate's slot exec role, referenced by slot task-defs via ARN — MUST add
18
18
  * the explicit `logs` / `secretsmanager` statements itself.
19
+ *
20
+ * `path`/`roleName` pin the IAM path and physical name — needed when an IAM
21
+ * grant fences on the role's path (the dev-role `iam:PassRole` grant on
22
+ * `role/fjall/dev/*`), since the CDK default is path `/` + a CFN-generated
23
+ * name no path- or prefix-scoped grant can match.
19
24
  */
20
25
  export declare function createBaseExecutionRole(scope: Construct, id: string, opts?: {
21
26
  ssmSecretsPath?: string;
27
+ path?: string;
28
+ roleName?: string;
22
29
  }): Role;
23
30
  /**
24
31
  * Creates the execution role for ECS infrastructure operations.
@@ -15,10 +15,17 @@ import { deriveSsmSecretsPath } from "./ecsTaskDefinition.js";
15
15
  * SHARED or IMPORTED role those auto-grants cannot reach — e.g. the dev
16
16
  * substrate's slot exec role, referenced by slot task-defs via ARN — MUST add
17
17
  * the explicit `logs` / `secretsmanager` statements itself.
18
+ *
19
+ * `path`/`roleName` pin the IAM path and physical name — needed when an IAM
20
+ * grant fences on the role's path (the dev-role `iam:PassRole` grant on
21
+ * `role/fjall/dev/*`), since the CDK default is path `/` + a CFN-generated
22
+ * name no path- or prefix-scoped grant can match.
18
23
  */
19
24
  export function createBaseExecutionRole(scope, id, opts = {}) {
20
25
  const executionRole = new Role(scope, id, {
21
- assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com")
26
+ assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com"),
27
+ path: opts.path,
28
+ roleName: opts.roleName
22
29
  });
23
30
  // GetAuthorizationToken is an account-level API that requires resources: ["*"].
24
31
  // The image-pull actions also use "*" because ecrRepository can be a string URI
@@ -20,6 +20,13 @@ interface RdsProps {
20
20
  clusterIdentifier?: string;
21
21
  /** Physical name of the generated master-credentials secret. */
22
22
  credentialsSecretName?: string;
23
+ /**
24
+ * KMS alias for the CMK encrypting the master-credentials secret. Aliases are
25
+ * account+region-unique, so a caller with a fixed `databaseName` deployed once
26
+ * per app (e.g. the dev substrate) must pass an app-scoped alias or the second
27
+ * app's deploy fails on the alias collision.
28
+ */
29
+ credentialsKmsAliasName?: string;
23
30
  /** Serverless-v2 floor in ACU. 0 enables scale-to-zero (auto-pause). */
24
31
  serverlessV2MinCapacity?: number;
25
32
  /** Serverless-v2 ceiling in ACU. */
@@ -100,6 +100,7 @@ export class RdsAurora extends Construct {
100
100
  this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
101
101
  secretName: props.credentialsSecretName ??
102
102
  ResourceNaming.credentialsSecretName(id),
103
+ aliasName: props.credentialsKmsAliasName,
103
104
  generateSecretString: {
104
105
  secretStringTemplate: JSON.stringify({ username }),
105
106
  excludePunctuation: true,
@@ -151,7 +152,7 @@ export class RdsAurora extends Construct {
151
152
  const readers = this.buildReaders(props, piEnabled, performanceInsightsKey, performanceInsightsRetention);
152
153
  const engine = props.engine ||
153
154
  DatabaseClusterEngine.auroraPostgres({
154
- version: AuroraPostgresEngineVersion.of("16.6", "16")
155
+ version: AuroraPostgresEngineVersion.of("16.10", "16")
155
156
  });
156
157
  const parameterGroup = new ParameterGroup(this, `${this.databaseNameValue}ParameterGroup`, {
157
158
  engine,
@@ -16,15 +16,18 @@ export declare const RDS_DEFAULTS: Readonly<{
16
16
  * (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
17
17
  * idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
18
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`).
19
+ * pins the cluster above 0 ACU). Engine 16.10 satisfies the auto-pause-capable
20
+ * floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`). AWS removes old minors from
21
+ * the creatable list over time (16.6 rejected with "Cannot find version" by
22
+ * 2026-07) — pin a mid-window minor, not the oldest available, and expect this
23
+ * to need a bump roughly yearly.
21
24
  */
22
25
  export declare const DEV_AURORA_DEFAULTS: Readonly<{
23
26
  readonly SERVERLESS_V2_MIN_CAPACITY: 0;
24
27
  readonly SERVERLESS_V2_MAX_CAPACITY: 4;
25
28
  readonly AUTO_PAUSE_SECONDS: 1200;
26
29
  readonly ENABLE_DATA_API: true;
27
- readonly ENGINE_VERSION: "16.6";
30
+ readonly ENGINE_VERSION: "16.10";
28
31
  }>;
29
32
  /**
30
33
  * Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
@@ -16,15 +16,18 @@ export const RDS_DEFAULTS = Object.freeze({
16
16
  * (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
17
17
  * idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
18
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`).
19
+ * pins the cluster above 0 ACU). Engine 16.10 satisfies the auto-pause-capable
20
+ * floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`). AWS removes old minors from
21
+ * the creatable list over time (16.6 rejected with "Cannot find version" by
22
+ * 2026-07) — pin a mid-window minor, not the oldest available, and expect this
23
+ * to need a bump roughly yearly.
21
24
  */
22
25
  export const DEV_AURORA_DEFAULTS = Object.freeze({
23
26
  SERVERLESS_V2_MIN_CAPACITY: 0,
24
27
  SERVERLESS_V2_MAX_CAPACITY: 4,
25
28
  AUTO_PAUSE_SECONDS: 1200,
26
29
  ENABLE_DATA_API: true,
27
- ENGINE_VERSION: "16.6"
30
+ ENGINE_VERSION: "16.10"
28
31
  });
29
32
  /**
30
33
  * Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
@@ -17,6 +17,12 @@ interface SecretProps {
17
17
  */
18
18
  secretStringValue?: string;
19
19
  description?: string;
20
+ /**
21
+ * KMS alias for the wrapper-minted encryption CMK. Aliases are unique per
22
+ * account+region, so callers whose construct id is not account-unique (e.g.
23
+ * a fixed-name pattern deployed once per app) MUST pass an app-scoped alias.
24
+ * @default cmk/<construct id>
25
+ */
20
26
  aliasName?: string;
21
27
  generateSecretString?: SecretStringGenerator;
22
28
  /** Regions where this secret should be replicated (for Global Aurora) */
@@ -71,7 +71,7 @@ export class Secret extends Construct {
71
71
  }
72
72
  // Create KMS key for new secrets only
73
73
  const customerManagedKey = new CustomerManagedKey(this, `${id}CustomerManagedKey`, {
74
- aliasName: `cmk/${id}`
74
+ aliasName: props.aliasName ?? `cmk/${id}`
75
75
  });
76
76
  this.secretsCustomerManagedKey = customerManagedKey;
77
77
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "3.4.1",
3
+ "version": "3.5.2",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@aws-sdk/client-organizations": "^3.1038.0",
70
- "@fjall/generator": "^3.4.1",
71
- "@fjall/util": "^3.4.1",
70
+ "@fjall/generator": "^3.5.2",
71
+ "@fjall/util": "^3.5.2",
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": "6d8ef9582d5abe6a8199431815dae761887c18d4"
85
+ "gitHead": "f59755647b2fd81ff692a3545e8e199d93c3bdf8"
86
86
  }