cdk-local 0.5.0 → 0.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.
package/README.md CHANGED
@@ -22,7 +22,7 @@ cdk-local deliberately does NOT emulate AWS managed services. The bet is: keep d
22
22
  It also picks up where `sam local` leaves off:
23
23
 
24
24
  - **CDK-native** — point it at your CDK app's `cdk.json`. No SAM templates, no extra config files.
25
- - **Wider coverage** — Lambda (ZIP + container image), API Gateway REST v1 / HTTP v2 / Function URL / WebSocket API, ECS run-task, ECS service with Service Connect + Cloud Map.
25
+ - **Wider coverage** — Lambda (ZIP + container image, plus Function URL), API Gateway REST v1 / HTTP v2 / WebSocket API, ECS run-task, ECS service with Service Connect + Cloud Map.
26
26
  - **Real container images** — Lambda code runs in the real `public.ecr.aws/lambda/*` base image via Lambda Runtime Interface Emulator (RIE). ECS tasks run as real Docker containers. The only dependency is Docker.
27
27
 
28
28
  ## What runs locally, what doesn't
@@ -32,7 +32,7 @@ cdk-local runs your **application compute** locally in Docker, using your CDK ap
32
32
  **Runs locally (application compute):**
33
33
 
34
34
  - **Lambda functions** — your code in a real `public.ecr.aws/lambda/*` container via the Lambda Runtime Interface Emulator
35
- - **API Gateway** — REST v1 / HTTP v2 / Function URL / WebSocket served by a local HTTP server
35
+ - **HTTP APIs & Function URLs** — API Gateway REST v1 / HTTP v2 / WebSocket and Lambda Function URLs served by a local HTTP server
36
36
  - **ECS** — tasks and services as real Docker containers (awsvpc / Service Connect / Cloud Map registry)
37
37
  - **Authorizers** — Lambda authorizers, Cognito User Pool JWT verification, IAM SigV4 verification
38
38
 
@@ -66,14 +66,26 @@ cdkl invoke MyStack/MyFunction --event ./event.json
66
66
 
67
67
  ![cdkl invoke against a local sample CDK app — no AWS account, no deploy](assets/cdkl-invoke.gif)
68
68
 
69
- #### API Gateway — `start-api`
69
+ #### HTTP APIs & Function URLs — `start-api`
70
70
 
71
- Serve your API Gateway routes (REST v1 / HTTP v2 / Function URL / WebSocket) on a local HTTP server.
71
+ Serve your app's HTTP surface locally — API Gateway routes (REST v1 / HTTP v2 / WebSocket) and Lambda Function URLs on a local HTTP server.
72
72
 
73
73
  ```bash
74
+ # Single-stack app: serve every API in the stack (each on its own port)
75
+ cdkl start-api
76
+
77
+ # Or target one API
74
78
  cdkl start-api MyStack/MyApi
75
79
  ```
76
80
 
81
+ In a multi-stack app, a bare `cdkl start-api` errors out rather than serving every stack's API at once (so a side-stack's API is never booted by accident). Pick the synth stack with the first of these you supply:
82
+
83
+ - `--stack <name>` — the synth stack name, matched against your CDK app.
84
+ - `--from-cfn-stack <name>` — the deployed CloudFormation stack name; doubles as the stack selector (see [section 2](#2-bound-to-a-deployed-stack)).
85
+ - a stack-qualified target like `MyStack/MyApi` — the `MyStack` prefix selects the stack.
86
+
87
+ See [docs/cli-reference.md](docs/cli-reference.md) for the full precedence rules.
88
+
77
89
  #### ECS — `run-task` / `start-service`
78
90
 
79
91
  Run an ECS task definition once, or start a long-running service with Service Connect / Cloud Map registry.
@@ -91,7 +103,7 @@ Use this for fast iteration on Lambda code, API routing checks, and container ta
91
103
 
92
104
  Once your stack is deployed to AWS (via the AWS CDK CLI or any other tool), pass `--from-cfn-stack <StackName>` and cdk-local reads the deployed CloudFormation stack to inject real ARNs, Secrets values, and IAM credentials (resolved from your current AWS profile) into the local execution.
93
105
 
94
- #### API Gateway — `start-api` (the headline use case)
106
+ #### HTTP APIs & Function URLs — `start-api` (the headline use case)
95
107
 
96
108
  A local API talking to real AWS — point a frontend at it for end-to-end debugging, including real Cognito JWT verification.
97
109
 
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { i as createLocalInvokeCommand, n as createLocalRunTaskCommand, r as createLocalStartApiCommand, t as createLocalStartServiceCommand } from "./local-start-service-DquKcP26.js";
2
+ import { i as createLocalInvokeCommand, n as createLocalRunTaskCommand, r as createLocalStartApiCommand, t as createLocalStartServiceCommand } from "./local-start-service-2tcrICJq.js";
3
3
  import { Command } from "commander";
4
4
 
5
5
  //#region src/cli/index.ts
6
6
  const program = new Command();
7
- program.name("cdkl").description("Run AWS CDK stacks locally with Docker.").version("0.5.0");
7
+ program.name("cdkl").description("Run AWS CDK stacks locally with Docker.").version("0.5.2");
8
8
  program.addCommand(createLocalInvokeCommand());
9
9
  program.addCommand(createLocalStartApiCommand());
10
10
  program.addCommand(createLocalRunTaskCommand());
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types/state.ts","../src/local/state-resolver.ts","../src/local/local-state-provider.ts","../src/cli/commands/local-state-source.ts","../src/local/embed-config.ts","../src/cli/commands/local-invoke.ts","../src/cli/commands/local-start-api.ts","../src/cli/commands/local-run-task.ts","../src/cli/commands/local-start-service.ts","../src/local/cfn-local-state-provider.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types/state.ts","../src/local/state-resolver.ts","../src/local/local-state-provider.ts","../src/cli/commands/local-state-source.ts","../src/local/embed-config.ts","../src/cli/commands/local-invoke.ts","../src/cli/commands/local-start-api.ts","../src/cli/commands/local-run-task.ts","../src/cli/commands/local-start-service.ts","../src/local/cfn-local-state-provider.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as LocalStateSourceError, c as rejectExplicitCfnStackWithMultipleStacks, d as CfnLocalStateProvider, i as createLocalInvokeCommand, l as resolveCfnRegion, n as createLocalRunTaskCommand, o as createLocalStateProvider, r as createLocalStartApiCommand, s as isCfnFlagPresent, t as createLocalStartServiceCommand, u as resolveCfnStackName } from "./local-start-service-DquKcP26.js";
1
+ import { a as LocalStateSourceError, c as rejectExplicitCfnStackWithMultipleStacks, d as CfnLocalStateProvider, i as createLocalInvokeCommand, l as resolveCfnRegion, n as createLocalRunTaskCommand, o as createLocalStateProvider, r as createLocalStartApiCommand, s as isCfnFlagPresent, t as createLocalStartServiceCommand, u as resolveCfnStackName } from "./local-start-service-2tcrICJq.js";
2
2
 
3
3
  export { CfnLocalStateProvider, LocalStateSourceError, createLocalInvokeCommand, createLocalRunTaskCommand, createLocalStartApiCommand, createLocalStartServiceCommand, createLocalStateProvider, isCfnFlagPresent, rejectExplicitCfnStackWithMultipleStacks, resolveCfnRegion, resolveCfnStackName };
@@ -7,7 +7,7 @@ import { Command, Option } from "commander";
7
7
  import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
8
8
  import { AssetManifestArtifact } from "@aws-cdk/cloud-assembly-api";
9
9
  import { CdkAppMultiContext, NonInteractiveIoHost, Toolkit } from "@aws-cdk/toolkit-lib";
10
- import { CloudFormationClient, DescribeStackResourcesCommand, DescribeStacksCommand, ListExportsCommand } from "@aws-sdk/client-cloudformation";
10
+ import { CloudFormationClient, DescribeStacksCommand, ListExportsCommand, ListStackResourcesCommand } from "@aws-sdk/client-cloudformation";
11
11
  import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
12
12
  import { Readable } from "node:stream";
13
13
  import { pipeline } from "node:stream/promises";
@@ -517,7 +517,7 @@ function resolveCdkPathToLogicalIds(input, index) {
517
517
  * --from-cfn-stack` (issue #606).
518
518
  *
519
519
  * The shape mirrors the SAM CLI's `sam local invoke --stack-name X`
520
- * behavior: reach into a deployed CFn stack via `DescribeStackResources`
520
+ * behavior: reach into a deployed CFn stack via `ListStackResources`
521
521
  * to look up physical IDs of every same-stack resource, then make those
522
522
  * IDs available to the existing `state-resolver.ts` substitution engine.
523
523
  * This lets `cdkl *` substitute env vars / secrets / images that
@@ -528,11 +528,11 @@ function resolveCdkPathToLogicalIds(input, index) {
528
528
  * Wire-format mapping:
529
529
  *
530
530
  * - `Ref: <LogicalId>` → resolved via the synthetic `ResourceState`
531
- * map built from `DescribeStackResources.StackResources[]` (one
531
+ * map built from `ListStackResources.StackResourceSummaries[]` (one
532
532
  * entry per `(LogicalResourceId, PhysicalResourceId, ResourceType)`
533
533
  * tuple).
534
534
  * - `Fn::GetAtt: [<LogicalId>, <Attr>]` → **warn-and-drop**. CFn's
535
- * `DescribeStackResources` does NOT return per-attribute values
535
+ * `ListStackResources` does NOT return per-attribute values
536
536
  * and the v1 policy (issue #606 recommendation (a)) is to surface
537
537
  * a per-key warn instead of pulling in the full provisioning layer
538
538
  * to call provider-specific describe APIs (e.g. `GetQueueAttributes`
@@ -557,8 +557,14 @@ function resolveCdkPathToLogicalIds(input, index) {
557
557
  *
558
558
  * AWS API contract notes:
559
559
  *
560
- * - `DescribeStackResources` is unpaginated up to 500 resources (CFn's
561
- * hard stack cap). One call suffices for the entire stack.
560
+ * - `ListStackResources` is paginated and returns EVERY resource in
561
+ * the stack across pages. We deliberately do NOT use
562
+ * `DescribeStackResources` here: that API returns only the first
563
+ * 100 resources (AWS-documented hard cap) with no `NextToken`, so a
564
+ * stack with > 100 resources silently loses its tail — every `Ref`
565
+ * to a dropped resource then warn-and-drops its env var. The
566
+ * provider walks `ListStackResources`'s `NextToken` until the page
567
+ * set is exhausted so all resources are mapped regardless of count.
562
568
  * - `DescribeStacks` is unpaginated when called with `StackName`.
563
569
  * - `ListExports` is paginated; the provider walks `NextToken` until
564
570
  * the page set is exhausted.
@@ -602,9 +608,9 @@ var CfnLocalStateProvider = class {
602
608
  const client = this.getClient();
603
609
  let resourceMap;
604
610
  try {
605
- resourceMap = buildResourceStateMap((await client.send(new DescribeStackResourcesCommand({ StackName: this.cfnStackName }))).StackResources ?? []);
611
+ resourceMap = buildResourceStateMap(await fetchAllStackResources(client, this.cfnStackName));
606
612
  } catch (err) {
607
- logger.warn(`${this.label}: DescribeStackResources(${this.cfnStackName}) failed: ${formatAwsErrorForWarn(err)}. Was the stack deployed in region '${this.region}'? Falling back.`);
613
+ logger.warn(`${this.label}: ListStackResources(${this.cfnStackName}) failed: ${formatAwsErrorForWarn(err)}. Was the stack deployed in region '${this.region}'? Falling back.`);
608
614
  return;
609
615
  }
610
616
  let outputs;
@@ -673,7 +679,7 @@ var CfnLocalStateProvider = class {
673
679
  };
674
680
  /**
675
681
  * Build the synthetic per-logical-id resource map from
676
- * `DescribeStackResources` output. Each `ResourceState` carries the
682
+ * `ListStackResources` output. Each `ResourceState` carries the
677
683
  * physical id (covers `Ref`) and the resource type; `attributes` is
678
684
  * left empty per issue #606's (a) recommendation — the warn-and-drop
679
685
  * policy on unresolvable `Fn::GetAtt` is the v1 contract. The other
@@ -712,6 +718,34 @@ function buildOutputsMap(outputs) {
712
718
  return out;
713
719
  }
714
720
  /**
721
+ * Walk `ListStackResources` until every page is consumed and return the
722
+ * full `StackResourceSummary[]`. `ListStackResources` is paginated and
723
+ * returns up to 100 resources per page; the provider must walk every
724
+ * page so stacks with more than 100 resources are mapped completely.
725
+ *
726
+ * This is the reason the provider does not use `DescribeStackResources`:
727
+ * that API caps its response at the first 100 resources with no
728
+ * `NextToken`, so a > 100-resource stack silently loses its tail and the
729
+ * dropped resources' `Ref`s become unresolvable. Exported for unit
730
+ * testing.
731
+ */
732
+ async function fetchAllStackResources(client, stackName) {
733
+ const out = [];
734
+ let nextToken;
735
+ let pages = 0;
736
+ do {
737
+ const resp = await client.send(new ListStackResourcesCommand({
738
+ StackName: stackName,
739
+ ...nextToken !== void 0 && { NextToken: nextToken }
740
+ }));
741
+ for (const summary of resp.StackResourceSummaries ?? []) out.push(summary);
742
+ nextToken = resp.NextToken;
743
+ pages += 1;
744
+ if (pages > 100) throw new Error("ListStackResources pagination exceeded 100 pages — likely a malformed NextToken loop.");
745
+ } while (nextToken !== void 0 && nextToken !== "");
746
+ return out;
747
+ }
748
+ /**
715
749
  * Walk `ListExports` until every page is consumed and return the
716
750
  * `Name -> Value` map. Same-region only (CFn exports are
717
751
  * region-scoped); the caller picks the region at provider
@@ -768,7 +802,7 @@ function formatAwsErrorForWarn(err) {
768
802
  * Built-in flag (always wired):
769
803
  *
770
804
  * - `--from-cfn-stack [<cfn-stack-name>]` — CFn-backed; reads a
771
- * deployed CloudFormation stack via `DescribeStackResources`.
805
+ * deployed CloudFormation stack via `ListStackResources`.
772
806
  *
773
807
  * Host-extensible state sources (via the `extraStateProviders` option):
774
808
  *
@@ -2016,7 +2050,7 @@ function resolveRef(arg, context) {
2016
2050
  const resource = context.resources[arg];
2017
2051
  if (!resource) return {
2018
2052
  kind: "unresolved",
2019
- reason: `Ref '${arg}': no record in cdkd state (was the resource deployed?)`
2053
+ reason: `Ref '${arg}': no record in the state source (was the resource deployed?)`
2020
2054
  };
2021
2055
  return {
2022
2056
  kind: "literal",
@@ -2026,7 +2060,7 @@ function resolveRef(arg, context) {
2026
2060
  function resolvePseudoParameter(name, pseudo) {
2027
2061
  if (!pseudo) return {
2028
2062
  kind: "unresolved",
2029
- reason: `Ref '${name}': pseudo parameter not supplied (need --from-state context)`
2063
+ reason: `Ref '${name}': pseudo parameter not supplied (need an active state source, e.g. --from-cfn-stack)`
2030
2064
  };
2031
2065
  switch (name) {
2032
2066
  case "AWS::AccountId":
@@ -2088,12 +2122,12 @@ function resolveGetAtt(arg, context) {
2088
2122
  const resource = context.resources[logicalId];
2089
2123
  if (!resource) return {
2090
2124
  kind: "unresolved",
2091
- reason: `Fn::GetAtt '${logicalId}.${attr}': no record in cdkd state`
2125
+ reason: `Fn::GetAtt '${logicalId}.${attr}': no record in the state source`
2092
2126
  };
2093
2127
  const cached = resource.attributes?.[attr];
2094
2128
  if (cached === void 0) return {
2095
2129
  kind: "unresolved",
2096
- reason: `Fn::GetAtt '${logicalId}.${attr}': attribute not captured in cdkd state at deploy time`
2130
+ reason: `Fn::GetAtt '${logicalId}.${attr}': attribute not captured in the state source at deploy time`
2097
2131
  };
2098
2132
  if (typeof cached === "string" || typeof cached === "number" || typeof cached === "boolean") return {
2099
2133
  kind: "literal",
@@ -2383,7 +2417,7 @@ async function resolveImportValueAsync(arg, context) {
2383
2417
  const exportName = inner.value;
2384
2418
  if (!context.crossStackResolver) return {
2385
2419
  kind: "unresolved",
2386
- reason: `Fn::ImportValue '${exportName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
2420
+ reason: `Fn::ImportValue '${exportName}': no cross-stack resolver supplied (pass a state-source flag, e.g. --from-cfn-stack, and ensure the producer stack was deployed)`
2387
2421
  };
2388
2422
  let resolved;
2389
2423
  try {
@@ -2396,7 +2430,7 @@ async function resolveImportValueAsync(arg, context) {
2396
2430
  }
2397
2431
  if (resolved === void 0) return {
2398
2432
  kind: "unresolved",
2399
- reason: `Fn::ImportValue '${exportName}': export not found in any cdkd-managed stack in this region`
2433
+ reason: `Fn::ImportValue '${exportName}': export not found in any deployed stack in this region`
2400
2434
  };
2401
2435
  return {
2402
2436
  kind: "literal",
@@ -2454,11 +2488,11 @@ async function resolveGetStackOutputAsync(arg, context) {
2454
2488
  };
2455
2489
  if (args["RoleArn"] !== void 0 && args["RoleArn"] !== null) return {
2456
2490
  kind: "unresolved",
2457
- reason: `Fn::GetStackOutput '${stackName}.${outputName}': RoleArn (cross-account) is not yet supported by --from-state — tracked under issue #449`
2491
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': RoleArn (cross-account) is not yet supported by the state source — tracked under issue #449`
2458
2492
  };
2459
2493
  if (!context.crossStackResolver) return {
2460
2494
  kind: "unresolved",
2461
- reason: `Fn::GetStackOutput '${stackName}.${outputName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
2495
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': no cross-stack resolver supplied (pass a state-source flag, e.g. --from-cfn-stack, and ensure the producer stack was deployed)`
2462
2496
  };
2463
2497
  let resolved;
2464
2498
  try {
@@ -3768,7 +3802,7 @@ async function runDetached(opts) {
3768
3802
  const ro = mount.readOnly ? ":ro" : "";
3769
3803
  args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
3770
3804
  }
3771
- for (const [k, v] of Object.entries(opts.env)) args.push("-e", `${k}=${v}`);
3805
+ const passthroughEnv = appendEnvFlags(args, opts.env, SENSITIVE_ENV_KEYS);
3772
3806
  if (opts.tmpfs) args.push("--tmpfs", `${opts.tmpfs.target}:rw,size=${opts.tmpfs.sizeMb}m`);
3773
3807
  if (opts.workingDir) args.push("--workdir", opts.workingDir);
3774
3808
  let entryPointTail = [];
@@ -3779,7 +3813,10 @@ async function runDetached(opts) {
3779
3813
  args.push(opts.image, ...entryPointTail, ...opts.cmd);
3780
3814
  getLogger().child("docker").debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
3781
3815
  try {
3782
- const { stdout } = await execFileAsync$3(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
3816
+ const { stdout } = await execFileAsync$3(getDockerCmd(), args, {
3817
+ maxBuffer: 10 * 1024 * 1024,
3818
+ ...execEnvForSecrets(passthroughEnv)
3819
+ });
3783
3820
  return stdout.trim();
3784
3821
  } catch (error) {
3785
3822
  const err = error;
@@ -3874,23 +3911,66 @@ function pickFreePort() {
3874
3911
  });
3875
3912
  }
3876
3913
  /**
3877
- * AWS credential keys whose values must NOT be written to the debug log.
3878
- * `forwardAwsEnv` / `assumeLambdaExecutionRole` (in `local-invoke.ts`)
3879
- * push these via `-e <KEY>=<value>` flags into `runDetached`'s args
3880
- * array, and `cdkl invoke --verbose` would otherwise leak them
3881
- * into stdout / log files. Only the matching `-e <KEY>=<value>` pair is
3882
- * redacted; non-credential `-e KEY=val` entries pass through unchanged.
3914
+ * Env keys whose VALUES are sensitive (AWS credentials). These are
3915
+ * routed through docker's value-from-process-env form by
3916
+ * {@link appendEnvFlags} so the value never appears in `docker run`'s
3917
+ * argv (`ps` / `/proc/<pid>/cmdline`), and they are also redacted by
3918
+ * {@link redactAwsCredentialsInArgs} as a defense for any path that
3919
+ * still emits the inline `-e <KEY>=<value>` form into the debug log.
3883
3920
  */
3884
- const REDACTED_ENV_KEYS = new Set([
3921
+ const SENSITIVE_ENV_KEYS = new Set([
3885
3922
  "AWS_ACCESS_KEY_ID",
3886
3923
  "AWS_SECRET_ACCESS_KEY",
3887
3924
  "AWS_SESSION_TOKEN"
3888
3925
  ]);
3889
3926
  /**
3927
+ * Append `-e` flags for `env` to `args`, routing keys in `sensitiveKeys`
3928
+ * through docker's value-from-process-env form (`-e KEY`, with NO
3929
+ * `=value`). Docker reads the value from its OWN environment at run time,
3930
+ * so the secret never lands in `docker run`'s argv — invisible to
3931
+ * `ps aux` / `/proc/<pid>/cmdline` (other users on a shared host) and to
3932
+ * shell history. Non-sensitive keys keep the inline `-e KEY=VALUE` form
3933
+ * (fine for non-secret config, and keeps `--debug` output readable).
3934
+ *
3935
+ * Returns the `{ KEY: value }` map of routed-through keys; the caller MUST
3936
+ * merge it into the spawned docker process's `env` (see
3937
+ * {@link execEnvForSecrets}) so docker can resolve each passed-through
3938
+ * key. Values still appear in `docker inspect` Config.Env — that is
3939
+ * inherent to container env and matches production behavior.
3940
+ *
3941
+ * Unlike `--env-file`, this handles multi-line values (e.g. PEM secrets)
3942
+ * and needs no temp file on disk.
3943
+ */
3944
+ function appendEnvFlags(args, env, sensitiveKeys) {
3945
+ const passthrough = {};
3946
+ for (const [k, v] of Object.entries(env)) if (sensitiveKeys.has(k)) {
3947
+ args.push("-e", k);
3948
+ passthrough[k] = v;
3949
+ } else args.push("-e", `${k}=${v}`);
3950
+ return passthrough;
3951
+ }
3952
+ /**
3953
+ * Build the `env` option for an `execFile` / `spawn` call that runs a
3954
+ * `docker run` whose args include passed-through sensitive keys (from
3955
+ * {@link appendEnvFlags}). Returns `{ env: {...process.env, ...passthrough} }`
3956
+ * so docker inherits the normal environment PLUS the sensitive values it
3957
+ * must resolve, or `{}` when there is nothing to pass through (preserving
3958
+ * the default inherited-environment behavior).
3959
+ */
3960
+ function execEnvForSecrets(passthrough) {
3961
+ if (Object.keys(passthrough).length === 0) return {};
3962
+ return { env: {
3963
+ ...process.env,
3964
+ ...passthrough
3965
+ } };
3966
+ }
3967
+ /**
3890
3968
  * Returns a copy of `args` with any `-e <KEY>=<value>` pair whose KEY is
3891
- * in {@link REDACTED_ENV_KEYS} replaced with `-e <KEY>=***`. The actual
3969
+ * in {@link SENSITIVE_ENV_KEYS} replaced with `-e <KEY>=***`. The actual
3892
3970
  * `args` passed to `spawn` are never mutated — this is for log output
3893
- * only.
3971
+ * only. With {@link appendEnvFlags} routing credentials through the
3972
+ * value-less `-e KEY` form this rarely fires, but it stays as defense for
3973
+ * any inline-form credential that slips into a logged command.
3894
3974
  */
3895
3975
  function redactAwsCredentialsInArgs(args) {
3896
3976
  const out = [];
@@ -3901,7 +3981,7 @@ function redactAwsCredentialsInArgs(args) {
3901
3981
  const eqIdx = next.indexOf("=");
3902
3982
  if (eqIdx > 0) {
3903
3983
  const key = next.substring(0, eqIdx);
3904
- if (REDACTED_ENV_KEYS.has(key)) {
3984
+ if (SENSITIVE_ENV_KEYS.has(key)) {
3905
3985
  out.push("-e", `${key}=***`);
3906
3986
  i++;
3907
3987
  continue;
@@ -5490,7 +5570,7 @@ function pickReferencedLogicalId(intrinsic) {
5490
5570
  }
5491
5571
  function createLocalInvokeCommand(opts = {}) {
5492
5572
  setEmbedConfig(opts.embedConfig);
5493
- const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from state (requires an active state source); (3) `--no-assume-role` explicitly opts out. Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers. Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name; pass an explicit value when CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
5573
+ const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from state (requires an active state source); (3) `--no-assume-role` explicitly opts out. Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers. Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name; pass an explicit value when CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
5494
5574
  await localInvokeCommand(target, options, opts.extraStateProviders);
5495
5575
  }));
5496
5576
  [
@@ -15552,7 +15632,7 @@ function resolveMtlsConfig(options) {
15552
15632
  */
15553
15633
  function createLocalStartApiCommand(opts = {}) {
15554
15634
  setEmbedConfig(opts.embedConfig);
15555
- const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and AWS_IAM auth (REST v1 `AuthorizationType: AWS_IAM` and Function URL `AuthType: AWS_IAM` — SigV4 signature verification only; IAM policy evaluation is NOT emulated). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", `Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors \`${getEmbedConfig().cliName} invoke\` / \`${getEmbedConfig().cliName} run-task\` target syntax.`).addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in Lambda env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).addOption(new Option("--mtls-truststore <path>", `PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj "/CN=${getEmbedConfig().resourceNamePrefix}-ca" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj "/CN=localhost"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj "/CN=client"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...`)).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(async (target, options) => {
15635
+ const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and AWS_IAM auth (REST v1 `AuthorizationType: AWS_IAM` and Function URL `AuthType: AWS_IAM` — SigV4 signature verification only; IAM policy evaluation is NOT emulated). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", `Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors \`${getEmbedConfig().cliName} invoke\` / \`${getEmbedConfig().cliName} run-task\` target syntax.`).addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in Lambda env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).addOption(new Option("--mtls-truststore <path>", `PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj "/CN=${getEmbedConfig().resourceNamePrefix}-ca" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj "/CN=localhost"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj "/CN=client"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...`)).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(async (target, options) => {
15556
15636
  await localStartApiCommand(target, options, opts.extraStateProviders);
15557
15637
  }));
15558
15638
  [
@@ -15697,11 +15777,14 @@ async function createNetworkAndSidecar(args) {
15697
15777
  if (credentials.sessionToken) sidecarEnv["AWS_SESSION_TOKEN"] = credentials.sessionToken;
15698
15778
  }
15699
15779
  if (cluster) sidecarEnv["CLUSTER"] = cluster;
15700
- for (const [k, v] of Object.entries(sidecarEnv)) sidecarArgs.push("-e", `${k}=${v}`);
15780
+ const sidecarPassthroughEnv = appendEnvFlags(sidecarArgs, sidecarEnv, SENSITIVE_ENV_KEYS);
15701
15781
  sidecarArgs.push(METADATA_ENDPOINT_IMAGE);
15702
15782
  logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
15703
15783
  try {
15704
- const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, { maxBuffer: 10 * 1024 * 1024 });
15784
+ const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, {
15785
+ maxBuffer: 10 * 1024 * 1024,
15786
+ ...execEnvForSecrets(sidecarPassthroughEnv)
15787
+ });
15705
15788
  return stdout.trim();
15706
15789
  } catch (err) {
15707
15790
  await destroyNetworkOnly(networkName);
@@ -16034,11 +16117,14 @@ async function runEcsTask(task, options, state) {
16034
16117
  for (const containerName of startOrder) {
16035
16118
  const container = task.containers.find((c) => c.name === containerName);
16036
16119
  await awaitDependencies(container, startedByName);
16037
- const args = dockerCmds.get(container.name);
16120
+ const { args, sensitiveEnv } = dockerCmds.get(container.name);
16038
16121
  logger.info(`Starting container '${container.name}' (image=${imagePlan.get(container.name)})`);
16039
16122
  let id;
16040
16123
  try {
16041
- const { stdout } = await execFileAsync$1(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
16124
+ const { stdout } = await execFileAsync$1(getDockerCmd(), args, {
16125
+ maxBuffer: 10 * 1024 * 1024,
16126
+ ...execEnvForSecrets(sensitiveEnv)
16127
+ });
16042
16128
  id = stdout.trim();
16043
16129
  } catch (err) {
16044
16130
  const e = err;
@@ -16392,7 +16478,9 @@ function buildDockerRunArgs(opts) {
16392
16478
  applyOverrideMap(finalEnv, overrides["Parameters"]);
16393
16479
  applyOverrideMap(finalEnv, overrides[container.name]);
16394
16480
  }
16395
- for (const [k, v] of Object.entries(finalEnv)) args.push("-e", `${k}=${v}`);
16481
+ const sensitiveEnvKeys = new Set(SENSITIVE_ENV_KEYS);
16482
+ for (const s of secrets) sensitiveEnvKeys.add(s.name);
16483
+ const sensitiveEnv = appendEnvFlags(args, finalEnv, sensitiveEnvKeys);
16396
16484
  if (container.user) args.push("--user", container.user);
16397
16485
  if (container.privileged) args.push("--privileged");
16398
16486
  if (container.readonlyRootFilesystem) args.push("--read-only");
@@ -16412,7 +16500,10 @@ function buildDockerRunArgs(opts) {
16412
16500
  entryPointTail = container.entryPoint.slice(1);
16413
16501
  }
16414
16502
  args.push(image, ...entryPointTail, ...container.command ?? []);
16415
- return args;
16503
+ return {
16504
+ args,
16505
+ sensitiveEnv
16506
+ };
16416
16507
  }
16417
16508
  function applyOverrideMap(acc, map) {
16418
16509
  if (!map) return;
@@ -16700,7 +16791,7 @@ async function resolveSidecarCredentials(options, assumedCredentials) {
16700
16791
  }
16701
16792
  function createLocalRunTaskCommand(opts = {}) {
16702
16793
  setEmbedConfig(opts.embedConfig);
16703
- const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
16794
+ const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
16704
16795
  await localRunTaskCommand(target, options, opts.extraStateProviders);
16705
16796
  }));
16706
16797
  [
@@ -18094,7 +18185,7 @@ async function resolveSharedSidecarCredentials(options) {
18094
18185
  }
18095
18186
  function createLocalStartServiceCommand(opts = {}) {
18096
18187
  setEmbedConfig(opts.embedConfig);
18097
- const cmd = new Command("start-service").description(`Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as \`${getEmbedConfig().cliName} run-task\`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay.`).argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (targets, options) => {
18188
+ const cmd = new Command("start-service").description(`Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as \`${getEmbedConfig().cliName} run-task\`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay.`).argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (targets, options) => {
18098
18189
  await localStartServiceCommand(targets, options, opts.extraStateProviders);
18099
18190
  }));
18100
18191
  [
@@ -18108,4 +18199,4 @@ function createLocalStartServiceCommand(opts = {}) {
18108
18199
 
18109
18200
  //#endregion
18110
18201
  export { LocalStateSourceError as a, rejectExplicitCfnStackWithMultipleStacks as c, CfnLocalStateProvider as d, createLocalInvokeCommand as i, resolveCfnRegion as l, createLocalRunTaskCommand as n, createLocalStateProvider as o, createLocalStartApiCommand as r, isCfnFlagPresent as s, createLocalStartServiceCommand as t, resolveCfnStackName as u };
18111
- //# sourceMappingURL=local-start-service-DquKcP26.js.map
18202
+ //# sourceMappingURL=local-start-service-2tcrICJq.js.map