@go-to-k/cdkd 0.117.2 → 0.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -37297,8 +37297,11 @@ function buildSubstitutionContextFromImageContext(context) {
37297
37297
  * `physicalId`, and `Fn::GetAtt: [<Repo>, 'RepositoryUri']` shapes
37298
37298
  * are resolved via the same state record.
37299
37299
  *
37300
- * Tier 3 (cross-account / cross-region pull) is deferred — `pullEcrImage`
37301
- * surfaces the same workaround pointer it already does.
37300
+ * Cross-account / cross-region pull (#455): `pullEcrImage` auto-detects
37301
+ * cross-account from `sts:GetCallerIdentity` and authenticates against
37302
+ * the URI's region directly. Pass `--ecr-role-arn <arn>` when the caller
37303
+ * does not already have cross-account `ecr:GetAuthorizationToken` /
37304
+ * `ecr:BatchGetImage` access on the target repository.
37302
37305
  */
37303
37306
  function parseContainerImage(raw, containerName, taskLogicalId, resources, _stack, context) {
37304
37307
  const getAttImage = tryResolveImageGetAtt(raw, resources, context);
@@ -37969,16 +37972,26 @@ function redactAwsCredentialsInArgs(args) {
37969
37972
  //#endregion
37970
37973
  //#region src/local/ecr-puller.ts
37971
37974
  /**
37972
- * ECR pull fallback for `cdkd local invoke` against deployed container
37973
- * Lambdas (PR 5, D5.2). When `Code.ImageUri` resolves to an ECR URI but
37975
+ * ECR pull fallback for `cdkd local invoke` / `cdkd local start-api` /
37976
+ * `cdkd local run-task`. When the image URI resolves to an ECR repo but
37974
37977
  * doesn't match any cdk.out asset (typical when invoking a stack
37975
- * deployed elsewhere), cdkd attempts `docker pull` against the same
37976
- * account/region.
37977
- *
37978
- * **Same-account / same-region only**:
37979
- * - Cross-account requires AssumeRole + a different ECR client. Hard
37980
- * error with a pointer at the deferred follow-up PR.
37981
- * - Cross-region requires a region-aware ECR client. Same hard error.
37978
+ * deployed elsewhere or sharing a centralized registry), cdkd
37979
+ * authenticates against the target registry and runs `docker pull`.
37980
+ *
37981
+ * **Cross-account / cross-region** (#455):
37982
+ * - Same-account, same-region: fast path. No STS hop. The default
37983
+ * credential chain is used directly for `ecr:GetAuthorizationToken`.
37984
+ * - `ecrRoleArn` is provided: `sts:AssumeRole` is issued via the
37985
+ * default credential chain to obtain temporary credentials for the
37986
+ * target account. The resulting credentials authenticate the ECR
37987
+ * client (regardless of region — the ECR client is built for the
37988
+ * URI's region, which can differ from the caller's profile region).
37989
+ * - Cross-account, NO `ecrRoleArn`: cdkd falls through to the
37990
+ * default credential chain. This works when the caller has been
37991
+ * granted cross-account `ecr:GetAuthorizationToken` +
37992
+ * `ecr:BatchGetImage` permissions on the target repository via an
37993
+ * IAM policy; otherwise AWS rejects the call with `AccessDenied`
37994
+ * and the user is pointed at `--ecr-role-arn`.
37982
37995
  *
37983
37996
  * The `--no-pull` semantics (C3 in the design doc):
37984
37997
  * - When NOT set: `ecrLogin` + `docker pull <uri>`.
@@ -38005,34 +38018,87 @@ function parseEcrUri(imageUri) {
38005
38018
  };
38006
38019
  }
38007
38020
  /**
38008
- * Pull (or verify locally cached) a container Lambda image from ECR.
38021
+ * Module-level cache for STS-issued AssumeRole credentials, keyed by
38022
+ * `(ecrRoleArn, callerRegion)`. Closes the reviewer's MAJOR finding: ECS
38023
+ * run-task with N containers under one `--ecr-role-arn` would otherwise issue
38024
+ * N× `AssumeRole` and N× `GetCallerIdentity` for identical credentials valid
38025
+ * for 3600s. The cache keeps a 5-minute safety margin against the recorded
38026
+ * `Expiration` so STS-side / local-clock skew never lets a stale entry through.
38027
+ *
38028
+ * Cache key is intentionally `(roleArn, region)` rather than full caller
38029
+ * identity — STS issues per-region session creds, and a switch of `--region`
38030
+ * between two `local invoke` calls in the same process must re-issue.
38031
+ *
38032
+ * NOT cleared on process exit — Node's module scope evaporates with the
38033
+ * process, and no inter-process sharing is desired (each `cdkd local invoke`
38034
+ * is its own isolated runtime).
38035
+ */
38036
+ const ASSUMED_ROLE_CACHE = /* @__PURE__ */ new Map();
38037
+ /**
38038
+ * Module-level cache for `STS:GetCallerIdentity`. The result is identity-only
38039
+ * (`Account`) and invariant for the lifetime of the process under one set of
38040
+ * default credentials. Keyed by `callerRegion` to avoid a cross-region leak
38041
+ * when the caller flips `AWS_REGION` mid-process (STS is global but the SDK
38042
+ * uses regional endpoints; the result is invariant in practice, but we key
38043
+ * on region for safety).
38044
+ */
38045
+ const CALLER_IDENTITY_CACHE = /* @__PURE__ */ new Map();
38046
+ /** 5-minute safety margin against the recorded STS expiration timestamp. */
38047
+ const STS_CREDENTIAL_SAFETY_MARGIN_MS = 300 * 1e3;
38048
+ function isCredentialFresh(creds) {
38049
+ if (!creds.expiration) return false;
38050
+ return creds.expiration.getTime() - Date.now() > STS_CREDENTIAL_SAFETY_MARGIN_MS;
38051
+ }
38052
+ /**
38053
+ * Pull (or verify locally cached) a container image from ECR.
38009
38054
  *
38010
- * Verifies same-account / same-region against the caller's STS identity
38011
- * before issuing any docker command. Returns the image URI the caller
38012
- * should pass to `docker run` (same as the input — no rewriting).
38055
+ * Auto-detects cross-account from `STS:GetCallerIdentity` and assumes
38056
+ * the supplied role when set. Returns the image URI the caller should
38057
+ * pass to `docker run` (same as the input — no rewriting).
38013
38058
  */
38014
38059
  async function pullEcrImage(imageUri, options) {
38015
38060
  const logger = getLogger().child("ecr-puller");
38016
38061
  const parsed = parseEcrUri(imageUri);
38017
38062
  if (!parsed) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is not an ECR URI. cdkd local invoke v1 only authenticates against ECR for the deployed-image fallback path.`);
38018
- const sts = new STSClient({ region: parsed.region });
38019
- let callerAccount;
38020
- try {
38021
- const identity = await sts.send(new GetCallerIdentityCommand({}));
38022
- if (!identity.Account) throw new LocalInvokeBuildError("STS GetCallerIdentity returned no Account. Verify your AWS credentials.");
38023
- callerAccount = identity.Account;
38024
- } finally {
38025
- sts.destroy();
38026
- }
38027
- if (callerAccount !== parsed.accountId) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is in account ${parsed.accountId}, but the caller is ${callerAccount}. Cross-account ECR pull is not supported in cdkd local invoke v1 — deferred to a follow-up PR. Workaround: assume a role in the target account before invoking, or build the image locally with \`cdkd local invoke -a cdk.out\` (no ECR pull).`);
38028
38063
  const callerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
38029
- if (callerRegion && callerRegion !== parsed.region) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is in region ${parsed.region}, but the caller's region is ${callerRegion}. Cross-region ECR pull is not supported in cdkd local invoke v1 — deferred to a follow-up PR. Workaround: re-run with AWS_REGION=${parsed.region} set, or build the image locally with -a cdk.out.`);
38030
38064
  if (options.skipPull) {
38031
38065
  logger.info(`Skipping ECR pull (--no-pull). Verifying ${imageUri} is in local cache...`);
38032
38066
  await verifyImageInLocalCache(imageUri);
38033
38067
  return imageUri;
38034
38068
  }
38035
- const ecr = new ECRClient({ region: parsed.region });
38069
+ const callerIdentityKey = callerRegion ?? "_unset";
38070
+ let callerAccount = CALLER_IDENTITY_CACHE.get(callerIdentityKey);
38071
+ if (callerAccount === void 0) {
38072
+ const sts = new STSClient({ ...callerRegion && { region: callerRegion } });
38073
+ try {
38074
+ const identity = await sts.send(new GetCallerIdentityCommand({}));
38075
+ if (!identity.Account) throw new LocalInvokeBuildError("STS GetCallerIdentity returned no Account. Verify your AWS credentials.");
38076
+ callerAccount = identity.Account;
38077
+ CALLER_IDENTITY_CACHE.set(callerIdentityKey, callerAccount);
38078
+ } finally {
38079
+ sts.destroy();
38080
+ }
38081
+ }
38082
+ const crossAccount = callerAccount !== parsed.accountId;
38083
+ const crossRegion = callerRegion !== void 0 && callerRegion !== parsed.region;
38084
+ let assumed;
38085
+ if (options.ecrRoleArn) {
38086
+ const cacheKey = `${options.ecrRoleArn}|${callerRegion ?? "_unset"}`;
38087
+ const cached = ASSUMED_ROLE_CACHE.get(cacheKey);
38088
+ if (cached && isCredentialFresh(cached)) {
38089
+ assumed = cached;
38090
+ logger.debug(`Reusing cached AssumeRole credentials for ${options.ecrRoleArn}`);
38091
+ } else {
38092
+ assumed = await assumeRoleForEcr(options.ecrRoleArn, callerRegion, logger);
38093
+ ASSUMED_ROLE_CACHE.set(cacheKey, assumed);
38094
+ logger.info(`Assumed role ${options.ecrRoleArn} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
38095
+ }
38096
+ } else if (crossAccount) logger.info(`Cross-account ECR pull: image account ${parsed.accountId} != caller ${callerAccount}. Using the caller's credentials; pass --ecr-role-arn <arn> if AWS rejects with AccessDenied.`);
38097
+ if (crossRegion) logger.info(`Cross-region ECR pull: image region ${parsed.region} != caller ${callerRegion ?? "(unset)"}. Authenticating against the image region directly.`);
38098
+ const ecr = new ECRClient({
38099
+ region: parsed.region,
38100
+ ...assumed && { credentials: assumed }
38101
+ });
38036
38102
  try {
38037
38103
  await ecrLogin(ecr, parsed.accountId, parsed.region);
38038
38104
  } finally {
@@ -38047,10 +38113,39 @@ async function pullEcrImage(imageUri, options) {
38047
38113
  return imageUri;
38048
38114
  }
38049
38115
  /**
38050
- * Authenticate the local docker daemon against the same-account ECR
38051
- * registry. Mirrors `DockerAssetPublisher.ecrLogin` but stays in this
38052
- * module so the local-invoke path doesn't depend on the publisher's
38053
- * larger surface area.
38116
+ * Assume the supplied role via the SDK default credential chain and
38117
+ * return the resulting temporary credentials. The STS client is built
38118
+ * with the caller's profile region (or unset) STS is a global
38119
+ * service so the region is informational, but threading it through
38120
+ * mirrors the convention used by `src/utils/role-arn.ts`.
38121
+ */
38122
+ async function assumeRoleForEcr(roleArn, callerRegion, logger) {
38123
+ logger.debug(`Assuming role ${roleArn} for ECR pull...`);
38124
+ const sts = new STSClient({ ...callerRegion && { region: callerRegion } });
38125
+ try {
38126
+ const creds = (await sts.send(new AssumeRoleCommand({
38127
+ RoleArn: roleArn,
38128
+ RoleSessionName: `cdkd-local-ecr-${Date.now()}`,
38129
+ DurationSeconds: 3600
38130
+ }))).Credentials;
38131
+ if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError(`AssumeRole(${roleArn}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
38132
+ return {
38133
+ accessKeyId: creds.AccessKeyId,
38134
+ secretAccessKey: creds.SecretAccessKey,
38135
+ sessionToken: creds.SessionToken,
38136
+ ...creds.Expiration && { expiration: creds.Expiration }
38137
+ };
38138
+ } catch (err) {
38139
+ if (err instanceof LocalInvokeBuildError) throw err;
38140
+ throw new LocalInvokeBuildError(`Failed to assume role ${roleArn} for ECR pull: ${err instanceof Error ? err.message : String(err)}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
38141
+ } finally {
38142
+ sts.destroy();
38143
+ }
38144
+ }
38145
+ /**
38146
+ * Authenticate the local docker daemon against the target ECR registry.
38147
+ * Mirrors `DockerAssetPublisher.ecrLogin` but stays in this module so the
38148
+ * local-invoke path doesn't depend on the publisher's larger surface area.
38054
38149
  */
38055
38150
  async function ecrLogin(client, accountId, region) {
38056
38151
  getLogger().child("ecr-puller").debug(`ECR login (account=${accountId}, region=${region})`);
@@ -43745,7 +43840,8 @@ async function prepareOneImage(task, container, options) {
43745
43840
  return image.uri;
43746
43841
  case "ecr": return pullEcrImage(image.uri, {
43747
43842
  skipPull: options.skipPull,
43748
- ...options.region !== void 0 && { region: options.region }
43843
+ ...options.region !== void 0 && { region: options.region },
43844
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn }
43749
43845
  });
43750
43846
  case "cdk-asset": {
43751
43847
  const cdkOutDir = task.stack.assetManifestPath ? dirname(task.stack.assetManifestPath) : void 0;
@@ -43999,6 +44095,7 @@ async function localRunTaskCommand(target, options) {
43999
44095
  if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
44000
44096
  if (options.platform) runOpts.platformOverride = options.platform;
44001
44097
  if (options.region) runOpts.region = options.region;
44098
+ if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
44002
44099
  const result = await runEcsTask(task, runOpts, state);
44003
44100
  if (options.detach) {
44004
44101
  logger.info("Task containers started in detached mode; cdkd is exiting.");
@@ -44148,7 +44245,7 @@ function readEnvOverridesFile$1(filePath) {
44148
44245
  return parsed;
44149
44246
  }
44150
44247
  function createLocalRunTaskCommand() {
44151
- 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("cdkd-local")).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("--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-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localRunTaskCommand));
44248
+ 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("cdkd-local")).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 (#455). 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-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localRunTaskCommand));
44152
44249
  [
44153
44250
  ...commonOptions,
44154
44251
  ...appOptions,
@@ -44492,7 +44589,8 @@ async function resolveContainerImagePlan(lambda, options) {
44492
44589
  logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
44493
44590
  imageRef = await pullEcrImage(lambda.imageUri, {
44494
44591
  skipPull: options.pull === false,
44495
- ...options.region !== void 0 && { region: options.region }
44592
+ ...options.region !== void 0 && { region: options.region },
44593
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn }
44496
44594
  });
44497
44595
  }
44498
44596
  const tmpfs = resolveTmpfsForLambda(lambda);
@@ -44786,7 +44884,7 @@ function pickReferencedLogicalId(intrinsic) {
44786
44884
  */
44787
44885
  function createLocalCommand() {
44788
44886
  const local = new Command("local").description("Local execution of Lambda functions (RIE) and ECS task definitions (Docker required)");
44789
- 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 (closes the \"developer admin / function narrow\" skew). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). 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("--from-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
44887
+ 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 (closes the \"developer admin / function narrow\" skew). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). 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("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). 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-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
44790
44888
  [
44791
44889
  ...commonOptions,
44792
44890
  ...appOptions,
@@ -46074,7 +46172,7 @@ function reorderArgs(argv) {
46074
46172
  */
46075
46173
  async function main() {
46076
46174
  const program = new Command();
46077
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.117.2");
46175
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.118.0");
46078
46176
  program.addCommand(createBootstrapCommand());
46079
46177
  program.addCommand(createSynthCommand());
46080
46178
  program.addCommand(createListCommand());