@go-to-k/cdkd 0.121.0 → 0.123.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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-CuHRHcyW.js";
3
- import { $ as LocalInvokeBuildError, A as stringifyValue, B as resolveApp, C as DiffCalculator, Ct as withSkipPrefix, D as S3StateBackend, E as LockManager, F as runDockerForeground, G as warnDeprecatedNoPrefixCliFlag, H as resolveSkipPrefix, I as runDockerStreaming, J as resolveBucketRegion, K as AssemblyReader, L as Synthesizer, M as buildDockerImage, N as formatDockerLoginError, O as shouldRetainResource, P as getDockerCmd, R as getDefaultStateBucketName, S as IntrinsicFunctionResolver, St as generateResourceNameWithFallback, T as TemplateParser, U as resolveStateBucketWithDefault, V as resolveCaptureObservedState, W as resolveStateBucketWithDefaultAndSource, X as CdkdError, _ as normalizeAwsTagsToCfn, _t as runStackBuffered, a as withRetry, at as RouteDiscoveryError, b as CloudControlProvider, bt as PATTERN_B_RESOURCE_TYPES, c as cyan, d as red, f as yellow, ft as normalizeAwsError, g as matchesCdkPath, h as CDK_PATH_TAG, ht as getLogger, i as withResourceDeadline, it as ResourceUpdateNotSupportedError, j as WorkGraph, k as AssetPublisher, l as gray, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as ProvisioningError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as StackHasActiveImportsError, p as IAMRoleProvider, pt as withErrorHandling, r as DeployEngine, rt as ResourceTimeoutError, s as bold, st as StackTerminationProtectionError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as PartialFailureError, u as green, v as resolveExplicitPhysicalId, vt as getLiveRenderer, w as DagBuilder, wt as withStackName, x as assertRegionMatch, xt as generateResourceName, y as ProviderRegistry, yt as PATTERN_B_NAME_PROPERTIES, z as getLegacyStateBucketName } from "./deploy-engine-B2RZT3ai.js";
3
+ import { A as AssetPublisher, B as getLegacyStateBucketName, C as applyRoleArnIfSet, Ct as generateResourceNameWithFallback, D as LockManager, E as TemplateParser, F as getDockerCmd, G as resolveStateBucketWithDefaultAndSource, H as resolveCaptureObservedState, I as runDockerForeground, K as warnDeprecatedNoPrefixCliFlag, L as runDockerStreaming, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as formatDockerLoginError, R as Synthesizer, S as IntrinsicFunctionResolver, St as generateResourceName, T as DagBuilder, Tt as withStackName, U as resolveSkipPrefix, V as resolveApp, W as resolveStateBucketWithDefault, Y as resolveBucketRegion, Z as CdkdError, _ as normalizeAwsTagsToCfn, a as withRetry, at as ResourceUpdateNotSupportedError, b as CloudControlProvider, bt as PATTERN_B_NAME_PROPERTIES, c as cyan, ct as StackTerminationProtectionError, d as red, et as LocalInvokeBuildError, f as yellow, g as matchesCdkPath, gt as getLogger, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceTimeoutError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, mt as withErrorHandling, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as PartialFailureError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as RouteDiscoveryError, p as IAMRoleProvider, pt as normalizeAwsError, q as AssemblyReader, r as DeployEngine, rt as ProvisioningError, s as bold, st as StackHasActiveImportsError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as green, v as resolveExplicitPhysicalId, vt as runStackBuffered, w as DiffCalculator, wt as withSkipPrefix, x as assertRegionMatch, xt as PATTERN_B_RESOURCE_TYPES, y as ProviderRegistry, yt as getLiveRenderer, z as getDefaultStateBucketName } from "./deploy-engine-DWpeb9wT.js";
4
4
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
6
6
  import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
@@ -437,63 +437,6 @@ function effectiveAssumeRoleArn(logicalId, opt) {
437
437
  */
438
438
  const destroyOptions = [new Option("-f, --force", "Do not ask for confirmation before destroying the stacks").default(false), new Option("--remove-protection", "Bypass deletion protection on protected resources by flipping the per-resource protection flag off in-place before delete. Covers stack-level terminationProtection (CDK property) and resource-level protection on AWS::Logs::LogGroup, AWS::RDS::DBInstance, AWS::RDS::DBCluster, AWS::DocDB::DBCluster, AWS::Neptune::DBCluster, AWS::Neptune::DBInstance, AWS::DynamoDB::Table, AWS::EC2::Instance, AWS::Cognito::UserPool, AWS::AutoScaling::AutoScalingGroup, and AWS::ElasticLoadBalancingV2::LoadBalancer.").default(false)];
439
439
 
440
- //#endregion
441
- //#region src/utils/role-arn.ts
442
- /**
443
- * Resolve the role-arn argument (CLI flag or `CDKD_ROLE_ARN` env var) and,
444
- * when set, assume the role and write the resulting temporary credentials
445
- * into `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`
446
- * for the rest of the process.
447
- *
448
- * **Why env vars, not threaded credentials.** cdkd constructs ~13
449
- * independent `AwsClients` instances across deploy / destroy / state /
450
- * import / etc. paths (each with its own region, sometimes — e.g. the
451
- * state-bucket client lives in a different region from the provisioning
452
- * clients). Threading a `credentials` object through every site is high
453
- * churn for an opt-in flag. AWS SDK v3 reads the standard `AWS_*` env
454
- * vars at the top of its default credentials chain, so writing into them
455
- * once at the command's entry makes every later `new XxxClient()` pick
456
- * up the assumed-role credentials automatically without touching the
457
- * client construction sites.
458
- *
459
- * **Why cdkd needs admin-equivalent on the assumed role.** Unlike `cdk
460
- * deploy`, cdkd does NOT route through CloudFormation. There is no
461
- * cfn-exec-role to delegate to. Every IAM / EC2 / Lambda / etc. API
462
- * call is issued from the cdkd process directly. The role you pass to
463
- * `--role-arn` (or set in `CDKD_ROLE_ARN`) MUST therefore have
464
- * admin-equivalent permissions on the resources being deployed; CDK
465
- * CLI's `cdk-hnb659fds-deploy-role-*` is NOT sufficient — that role
466
- * only carries CFn + asset-publish permissions.
467
- *
468
- * Default session duration is 1 hour. For longer-running deploys, the
469
- * caller should re-issue the cdkd command (the in-flight credentials
470
- * stay valid until expiry, but a re-run is the simplest recovery for
471
- * the rare case where a deploy outlives them).
472
- */
473
- async function applyRoleArnIfSet(opts) {
474
- const roleArn = opts.roleArn || process.env["CDKD_ROLE_ARN"];
475
- if (!roleArn) return;
476
- const logger = getLogger().child("role-arn");
477
- logger.debug(`Assuming role ${roleArn}...`);
478
- const sts = new STSClient({ ...opts.region && { region: opts.region } });
479
- try {
480
- const response = await sts.send(new AssumeRoleCommand({
481
- RoleArn: roleArn,
482
- RoleSessionName: `cdkd-${Date.now()}`,
483
- DurationSeconds: 3600
484
- }));
485
- if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${roleArn}`);
486
- const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
487
- if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${roleArn}`);
488
- process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
489
- process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
490
- process.env["AWS_SESSION_TOKEN"] = SessionToken;
491
- logger.info(`Assumed role ${roleArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
492
- } finally {
493
- sts.destroy();
494
- }
495
- }
496
-
497
440
  //#endregion
498
441
  //#region src/cli/commands/bootstrap.ts
499
442
  /**
@@ -35768,6 +35711,119 @@ async function loadStateForStack(stackName, synthRegion, opts) {
35768
35711
  resetAwsClients();
35769
35712
  }
35770
35713
  }
35714
+ /**
35715
+ * Build a {@link CrossStackResolver} that walks cdkd's S3 state to look
35716
+ * up `Fn::ImportValue` / `Fn::GetStackOutput` references the same way
35717
+ * `cdkd deploy`'s `IntrinsicFunctionResolver` does. Returns `undefined`
35718
+ * when the state bucket cannot be resolved (warn + fall back; matches
35719
+ * `loadStateForStack`'s policy).
35720
+ *
35721
+ * The returned `dispose` closes the AWS clients owned by the resolver
35722
+ * when the caller is done — callers MUST call it (typically in a
35723
+ * `try / finally`) so the per-request S3 client isn't leaked across the
35724
+ * CLI's lifetime.
35725
+ *
35726
+ * Why a separate AwsClients instance from `loadStateForStack`: the
35727
+ * existing helper destroys its clients in a `finally` immediately after
35728
+ * loading the consumer stack's state. The cross-stack resolver lives
35729
+ * longer — every env-var that references a cross-stack output triggers a
35730
+ * new state read. Owning a fresh `AwsClients` here gives the resolver
35731
+ * an independent lifetime managed by the caller.
35732
+ *
35733
+ * Same-account / same-region only in v1 (the resolver's `producerRegion`
35734
+ * arg is honored, but only for state lookups within the same cdkd state
35735
+ * bucket). Cross-region `Fn::ImportValue` is tracked under #451;
35736
+ * cross-account `Fn::GetStackOutput.RoleArn` is tracked under #449.
35737
+ */
35738
+ async function buildCrossStackResolver(consumerRegion, opts) {
35739
+ const logger = getLogger();
35740
+ const prefix = opts.logPrefix ?? "--from-state";
35741
+ let stateBucket;
35742
+ try {
35743
+ stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, consumerRegion);
35744
+ } catch (err) {
35745
+ logger.warn(`${prefix}: cross-stack resolver could not resolve state bucket: ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue / Fn::GetStackOutput env entries will warn-and-drop.`);
35746
+ return;
35747
+ }
35748
+ const awsClients = new AwsClients({
35749
+ ...opts.region !== void 0 && { region: opts.region },
35750
+ ...opts.profile !== void 0 && { profile: opts.profile }
35751
+ });
35752
+ const stateConfig = {
35753
+ bucket: stateBucket,
35754
+ prefix: opts.statePrefix
35755
+ };
35756
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
35757
+ ...opts.region !== void 0 && { region: opts.region },
35758
+ ...opts.profile !== void 0 && { profile: opts.profile }
35759
+ });
35760
+ try {
35761
+ await stateBackend.verifyBucketExists();
35762
+ } catch (err) {
35763
+ awsClients.destroy();
35764
+ logger.warn(`${prefix}: cross-stack resolver could not access state bucket '${stateBucket}': ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue / Fn::GetStackOutput env entries will warn-and-drop.`);
35765
+ return;
35766
+ }
35767
+ const exportIndex = new ExportIndexStore(awsClients.s3, stateBucket, opts.statePrefix, consumerRegion, stateBackend);
35768
+ return {
35769
+ resolver: {
35770
+ async resolveImport(exportName) {
35771
+ try {
35772
+ const entry = await exportIndex.lookup(exportName);
35773
+ if (entry) {
35774
+ const value = entry.value;
35775
+ if (typeof value === "string") return value;
35776
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
35777
+ return JSON.stringify(value);
35778
+ }
35779
+ } catch (err) {
35780
+ logger.debug(`${prefix}: exports index lookup failed for '${exportName}': ${err instanceof Error ? err.message : String(err)}; falling back to per-stack state scan`);
35781
+ }
35782
+ let refs;
35783
+ try {
35784
+ refs = await stateBackend.listStacks();
35785
+ } catch (err) {
35786
+ logger.debug(`${prefix}: failed to list stacks during Fn::ImportValue fallback for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
35787
+ return;
35788
+ }
35789
+ for (const ref of refs) {
35790
+ const region = ref.region ?? consumerRegion;
35791
+ if (region !== consumerRegion) continue;
35792
+ try {
35793
+ const got = await stateBackend.getState(ref.stackName, region);
35794
+ if (!got || !got.state.outputs) continue;
35795
+ if (exportName in got.state.outputs) {
35796
+ const value = got.state.outputs[exportName];
35797
+ if (typeof value === "string") return value;
35798
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
35799
+ return JSON.stringify(value);
35800
+ }
35801
+ } catch (err) {
35802
+ logger.debug(`${prefix}: state read failed for ${ref.stackName} (${region}) during Fn::ImportValue fallback: ${err instanceof Error ? err.message : String(err)}`);
35803
+ continue;
35804
+ }
35805
+ }
35806
+ },
35807
+ async resolveGetStackOutput(producerStack, producerRegion, outputName) {
35808
+ try {
35809
+ const got = await stateBackend.getState(producerStack, producerRegion);
35810
+ if (!got || !got.state.outputs) return void 0;
35811
+ if (!(outputName in got.state.outputs)) return void 0;
35812
+ const value = got.state.outputs[outputName];
35813
+ if (typeof value === "string") return value;
35814
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
35815
+ return JSON.stringify(value);
35816
+ } catch (err) {
35817
+ logger.debug(`${prefix}: state read failed for Fn::GetStackOutput '${producerStack}.${outputName}' (${producerRegion}): ${err instanceof Error ? err.message : String(err)}`);
35818
+ return;
35819
+ }
35820
+ }
35821
+ },
35822
+ dispose: () => {
35823
+ awsClients.destroy();
35824
+ }
35825
+ };
35826
+ }
35771
35827
 
35772
35828
  //#endregion
35773
35829
  //#region src/local/intrinsic-image.ts
@@ -36733,6 +36789,159 @@ function resolveJoin(arg, context) {
36733
36789
  };
36734
36790
  }
36735
36791
  /**
36792
+ * Async sibling of {@link substituteAgainstState}. Same semantics for every
36793
+ * intrinsic the sync path supports; additionally consults the
36794
+ * `crossStackResolver` (when supplied on the context) for `Fn::ImportValue`
36795
+ * and `Fn::GetStackOutput`.
36796
+ *
36797
+ * Callers that don't need cross-stack support should keep using the sync
36798
+ * helper. Code paths that wire `--from-state` env / secret substitution
36799
+ * (e.g. `cdkd local invoke --from-state`, `cdkd local run-task --from-state`)
36800
+ * route through this async version so a single env-var referencing a
36801
+ * cross-stack output is no longer warn-and-dropped.
36802
+ */
36803
+ async function substituteAgainstStateAsync(value, contextOrResources) {
36804
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
36805
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return {
36806
+ kind: "literal",
36807
+ value
36808
+ };
36809
+ if (value === null || typeof value !== "object") return {
36810
+ kind: "unresolved",
36811
+ reason: `unsupported value type: ${value === null ? "null" : typeof value}`
36812
+ };
36813
+ const obj = value;
36814
+ const keys = Object.keys(obj);
36815
+ if (keys.length !== 1) return {
36816
+ kind: "unresolved",
36817
+ reason: `expected an intrinsic with one key, got ${keys.length} keys`
36818
+ };
36819
+ const intrinsic = keys[0];
36820
+ const arg = obj[intrinsic];
36821
+ if (intrinsic === "Ref" || intrinsic === "Fn::GetAtt" || intrinsic === "Fn::Sub" || intrinsic === "Fn::Join") return substituteAgainstState(value, context);
36822
+ if (intrinsic === "Fn::ImportValue") return resolveImportValueAsync(arg, context);
36823
+ if (intrinsic === "Fn::GetStackOutput") return resolveGetStackOutputAsync(arg, context);
36824
+ return {
36825
+ kind: "unresolved",
36826
+ reason: `unsupported intrinsic '${intrinsic}' (supported: Ref, Fn::GetAtt, Fn::Sub, Fn::Join, Fn::ImportValue, Fn::GetStackOutput)`
36827
+ };
36828
+ }
36829
+ /**
36830
+ * `Fn::ImportValue: <exportName>` — the argument may itself be an
36831
+ * intrinsic that resolves to a string (e.g.
36832
+ * `{Fn::ImportValue: {Fn::Sub: 'MyStack-${AWS::Region}-Bucket'}}` — the
36833
+ * inner `Fn::Sub` is resolved against `pseudoParameters` first, then the
36834
+ * resulting string is looked up via the cross-stack resolver).
36835
+ */
36836
+ async function resolveImportValueAsync(arg, context) {
36837
+ const inner = substituteAgainstState(arg, context);
36838
+ if (inner.kind !== "literal") return {
36839
+ kind: "unresolved",
36840
+ reason: `Fn::ImportValue argument: ${inner.reason}`
36841
+ };
36842
+ if (typeof inner.value !== "string" || inner.value.length === 0) return {
36843
+ kind: "unresolved",
36844
+ reason: `Fn::ImportValue argument must resolve to a non-empty string, got ${typeof inner.value}`
36845
+ };
36846
+ const exportName = inner.value;
36847
+ if (!context.crossStackResolver) return {
36848
+ kind: "unresolved",
36849
+ reason: `Fn::ImportValue '${exportName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
36850
+ };
36851
+ let resolved;
36852
+ try {
36853
+ resolved = await context.crossStackResolver.resolveImport(exportName);
36854
+ } catch (err) {
36855
+ return {
36856
+ kind: "unresolved",
36857
+ reason: `Fn::ImportValue '${exportName}': lookup failed: ${err instanceof Error ? err.message : String(err)}`
36858
+ };
36859
+ }
36860
+ if (resolved === void 0) return {
36861
+ kind: "unresolved",
36862
+ reason: `Fn::ImportValue '${exportName}': export not found in any cdkd-managed stack in this region`
36863
+ };
36864
+ return {
36865
+ kind: "literal",
36866
+ value: resolved
36867
+ };
36868
+ }
36869
+ /**
36870
+ * `Fn::GetStackOutput: { StackName, OutputName, Region? }`. Same shape as
36871
+ * the deploy-engine resolver. `RoleArn` (cross-account) is intentionally
36872
+ * NOT supported in this path — the user-visible error message points at
36873
+ * the followup issue tracking it.
36874
+ */
36875
+ async function resolveGetStackOutputAsync(arg, context) {
36876
+ if (!arg || typeof arg !== "object" || Array.isArray(arg)) return {
36877
+ kind: "unresolved",
36878
+ reason: `Fn::GetStackOutput argument must be an object with StackName / OutputName / Region, got ${arg === null ? "null" : Array.isArray(arg) ? "array" : typeof arg}`
36879
+ };
36880
+ const args = arg;
36881
+ const stackNameSub = substituteAgainstState(args["StackName"], context);
36882
+ if (stackNameSub.kind !== "literal") return {
36883
+ kind: "unresolved",
36884
+ reason: `Fn::GetStackOutput.StackName: ${stackNameSub.reason}`
36885
+ };
36886
+ if (typeof stackNameSub.value !== "string" || stackNameSub.value.length === 0) return {
36887
+ kind: "unresolved",
36888
+ reason: `Fn::GetStackOutput.StackName must resolve to a non-empty string, got ${typeof stackNameSub.value}`
36889
+ };
36890
+ const stackName = stackNameSub.value;
36891
+ const outputNameSub = substituteAgainstState(args["OutputName"], context);
36892
+ if (outputNameSub.kind !== "literal") return {
36893
+ kind: "unresolved",
36894
+ reason: `Fn::GetStackOutput.OutputName: ${outputNameSub.reason}`
36895
+ };
36896
+ if (typeof outputNameSub.value !== "string" || outputNameSub.value.length === 0) return {
36897
+ kind: "unresolved",
36898
+ reason: `Fn::GetStackOutput.OutputName must resolve to a non-empty string, got ${typeof outputNameSub.value}`
36899
+ };
36900
+ const outputName = outputNameSub.value;
36901
+ let region;
36902
+ if (args["Region"] !== void 0 && args["Region"] !== null) {
36903
+ const regionSub = substituteAgainstState(args["Region"], context);
36904
+ if (regionSub.kind !== "literal") return {
36905
+ kind: "unresolved",
36906
+ reason: `Fn::GetStackOutput.Region: ${regionSub.reason}`
36907
+ };
36908
+ if (typeof regionSub.value !== "string" || regionSub.value.length === 0) return {
36909
+ kind: "unresolved",
36910
+ reason: `Fn::GetStackOutput.Region must resolve to a non-empty string, got ${typeof regionSub.value}`
36911
+ };
36912
+ region = regionSub.value;
36913
+ } else region = context.consumerRegion ?? context.pseudoParameters?.region;
36914
+ if (!region) return {
36915
+ kind: "unresolved",
36916
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': no Region supplied and consumer region is unknown (set --region, AWS_REGION, or env.region on the CDK stack)`
36917
+ };
36918
+ if (args["RoleArn"] !== void 0 && args["RoleArn"] !== null) return {
36919
+ kind: "unresolved",
36920
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': RoleArn (cross-account) is not yet supported by --from-state — tracked under issue #449`
36921
+ };
36922
+ if (!context.crossStackResolver) return {
36923
+ kind: "unresolved",
36924
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
36925
+ };
36926
+ let resolved;
36927
+ try {
36928
+ resolved = await context.crossStackResolver.resolveGetStackOutput(stackName, region, outputName);
36929
+ } catch (err) {
36930
+ return {
36931
+ kind: "unresolved",
36932
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): lookup failed: ${err instanceof Error ? err.message : String(err)}`
36933
+ };
36934
+ }
36935
+ if (resolved === void 0) return {
36936
+ kind: "unresolved",
36937
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): output not found in producer stack state`
36938
+ };
36939
+ return {
36940
+ kind: "literal",
36941
+ value: resolved
36942
+ };
36943
+ }
36944
+ /**
36736
36945
  * Build a pre-substituted env map from the template entry by feeding each
36737
36946
  * intrinsic value through `substituteAgainstState`. Literal entries pass
36738
36947
  * through untouched (the env-resolver handles them).
@@ -36773,6 +36982,50 @@ function substituteEnvVarsFromState(templateEnv, contextOrResources) {
36773
36982
  audit
36774
36983
  };
36775
36984
  }
36985
+ /**
36986
+ * Async sibling of {@link substituteEnvVarsFromState}. Routes every
36987
+ * intrinsic-valued entry through {@link substituteAgainstStateAsync} so
36988
+ * `Fn::ImportValue` / `Fn::GetStackOutput` resolve via the context's
36989
+ * `crossStackResolver` (when supplied). Mirrors the sync version in every
36990
+ * other respect: literal entries pass through unchanged, unresolved
36991
+ * entries are dropped with a per-key audit reason, and the env-resolver
36992
+ * sees the same "no template value" shape so the warn-and-drop path
36993
+ * fires consistently.
36994
+ *
36995
+ * Closes issue #454 — `cdkd local invoke --from-state` and
36996
+ * `cdkd local run-task --from-state` can now resolve cross-stack output
36997
+ * references in env vars / secrets instead of warn-and-dropping them.
36998
+ */
36999
+ async function substituteEnvVarsFromStateAsync(templateEnv, contextOrResources) {
37000
+ const env = {};
37001
+ const audit = {
37002
+ resolvedKeys: [],
37003
+ unresolved: []
37004
+ };
37005
+ if (!templateEnv) return {
37006
+ env,
37007
+ audit
37008
+ };
37009
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
37010
+ for (const [key, value] of Object.entries(templateEnv)) {
37011
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
37012
+ env[key] = value;
37013
+ continue;
37014
+ }
37015
+ const result = await substituteAgainstStateAsync(value, context);
37016
+ if (result.kind === "literal") {
37017
+ env[key] = result.value;
37018
+ audit.resolvedKeys.push(key);
37019
+ } else audit.unresolved.push({
37020
+ key,
37021
+ reason: result.reason
37022
+ });
37023
+ }
37024
+ return {
37025
+ env,
37026
+ audit
37027
+ };
37028
+ }
36776
37029
 
36777
37030
  //#endregion
36778
37031
  //#region src/local/ecs-task-resolver.ts
@@ -36816,6 +37069,7 @@ function detectEcsImageResolutionNeeds(stack) {
36816
37069
  let needsPseudoParameters = false;
36817
37070
  let needsStateResources = false;
36818
37071
  let needsEnvOrSecretSubstitution = false;
37072
+ let needsCrossStackResolver = false;
36819
37073
  for (const res of Object.values(resources)) {
36820
37074
  if (res.Type !== "AWS::ECS::TaskDefinition") continue;
36821
37075
  const props = res.Properties ?? {};
@@ -36828,6 +37082,10 @@ function detectEcsImageResolutionNeeds(stack) {
36828
37082
  if (need.pseudo) needsPseudoParameters = true;
36829
37083
  if (need.state) needsStateResources = true;
36830
37084
  if (containerHasIntrinsicEnvOrSecret(co)) needsEnvOrSecretSubstitution = true;
37085
+ if (containerHasCrossStackEnvOrSecret(co)) {
37086
+ needsEnvOrSecretSubstitution = true;
37087
+ needsCrossStackResolver = true;
37088
+ }
36831
37089
  }
36832
37090
  const rawVolumes = props["Volumes"];
36833
37091
  if (Array.isArray(rawVolumes)) {
@@ -36841,10 +37099,37 @@ function detectEcsImageResolutionNeeds(stack) {
36841
37099
  return {
36842
37100
  needsPseudoParameters,
36843
37101
  needsStateResources,
36844
- needsEnvOrSecretSubstitution
37102
+ needsEnvOrSecretSubstitution,
37103
+ needsCrossStackResolver
36845
37104
  };
36846
37105
  }
36847
37106
  /**
37107
+ * Returns true when any `Environment[].Value` or `Secrets[].ValueFrom`
37108
+ * is a top-level `Fn::ImportValue` / `Fn::GetStackOutput` intrinsic.
37109
+ * Issue #454 — gates cross-stack resolver construction so literal +
37110
+ * same-stack-intrinsic env / secret maps don't pay the extra cost.
37111
+ */
37112
+ function containerHasCrossStackEnvOrSecret(c) {
37113
+ const env = c["Environment"];
37114
+ if (Array.isArray(env)) for (const entry of env) {
37115
+ if (!entry || typeof entry !== "object") continue;
37116
+ const v = entry["Value"];
37117
+ if (isCrossStackIntrinsic(v)) return true;
37118
+ }
37119
+ const secrets = c["Secrets"];
37120
+ if (Array.isArray(secrets)) for (const entry of secrets) {
37121
+ if (!entry || typeof entry !== "object") continue;
37122
+ const v = entry["ValueFrom"];
37123
+ if (isCrossStackIntrinsic(v)) return true;
37124
+ }
37125
+ return false;
37126
+ }
37127
+ function isCrossStackIntrinsic(value) {
37128
+ if (!value || typeof value !== "object") return false;
37129
+ const obj = value;
37130
+ return "Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj;
37131
+ }
37132
+ /**
36848
37133
  * Detect whether a Volume entry has an intrinsic-valued `Host.SourcePath`.
36849
37134
  * Used by `detectEcsImageResolutionNeeds` (Gap 6 of #286) to trigger
36850
37135
  * state-load + pseudo-parameter resolution under `--from-state` when the
@@ -37589,6 +37874,87 @@ function checkVolumeHostPath(hostPath) {
37589
37874
  return false;
37590
37875
  }
37591
37876
  }
37877
+ /**
37878
+ * Async post-pass that walks the resolved task's container Environment +
37879
+ * Secrets entries from the raw template (preserved in `task.resource`)
37880
+ * and re-attempts substitution via {@link substituteAgainstStateAsync}
37881
+ * against the supplied context. The sync `parseContainerDefinition`
37882
+ * pass already substituted everything the legacy resolver handles; this
37883
+ * pass picks up the additional shapes the async resolver supports —
37884
+ * specifically `Fn::ImportValue` / `Fn::GetStackOutput` (issue #454).
37885
+ *
37886
+ * Resolved entries are patched onto the container's `environment` /
37887
+ * `secrets` map AND the corresponding `warnings` entries are filtered
37888
+ * out so the CLI doesn't print a stale per-container warn for an entry
37889
+ * the post-pass successfully resolved. Entries that STILL can't resolve
37890
+ * (e.g. producer stack not deployed) keep their original warning so the
37891
+ * CLI's UX matches the sync path.
37892
+ *
37893
+ * Pure-functional on the task structure outside of in-place mutation of
37894
+ * the container's `environment` / `secrets` / `warnings` arrays. The
37895
+ * task is expected to be the same `ResolvedEcsTask` instance returned
37896
+ * by `resolveEcsTaskTarget` — the runner downstream reads from these
37897
+ * fields directly.
37898
+ */
37899
+ async function applyCrossStackResolverToTask(task, context) {
37900
+ if (!context.crossStackResolver) return;
37901
+ const rawContainers = (task.resource.Properties ?? {})["ContainerDefinitions"];
37902
+ if (!Array.isArray(rawContainers)) return;
37903
+ for (let idx = 0; idx < task.containers.length; idx += 1) {
37904
+ const container = task.containers[idx];
37905
+ const raw = rawContainers[idx];
37906
+ if (!raw || typeof raw !== "object") continue;
37907
+ const c = raw;
37908
+ const containerName = pickString(c["Name"]) ?? container.name;
37909
+ const resolvedEnvKeys = /* @__PURE__ */ new Set();
37910
+ const resolvedSecretNames = /* @__PURE__ */ new Set();
37911
+ if (Array.isArray(c["Environment"])) for (const entry of c["Environment"]) {
37912
+ if (!entry || typeof entry !== "object") continue;
37913
+ const e = entry;
37914
+ const key = pickString(e["Name"]);
37915
+ const value = e["Value"];
37916
+ if (!key) continue;
37917
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") continue;
37918
+ if (key in container.environment) continue;
37919
+ if (!isCrossStackIntrinsic(value)) continue;
37920
+ const sub = await substituteAgainstStateAsync(value, context);
37921
+ if (sub.kind === "literal") {
37922
+ container.environment[key] = String(sub.value);
37923
+ resolvedEnvKeys.add(key);
37924
+ }
37925
+ }
37926
+ if (Array.isArray(c["Secrets"])) for (const entry of c["Secrets"]) {
37927
+ if (!entry || typeof entry !== "object") continue;
37928
+ const e = entry;
37929
+ const sName = pickString(e["Name"]);
37930
+ const valueFromRaw = e["ValueFrom"];
37931
+ if (!sName) continue;
37932
+ if (typeof valueFromRaw === "string" && valueFromRaw.length > 0) continue;
37933
+ if (container.secrets.some((s) => s.name === sName)) continue;
37934
+ if (!isCrossStackIntrinsic(valueFromRaw)) continue;
37935
+ const sub = await substituteAgainstStateAsync(valueFromRaw, context);
37936
+ if (sub.kind === "literal" && typeof sub.value === "string" && sub.value.length > 0) {
37937
+ container.secrets.push({
37938
+ name: sName,
37939
+ valueFrom: sub.value
37940
+ });
37941
+ resolvedSecretNames.add(sName);
37942
+ }
37943
+ }
37944
+ if (resolvedEnvKeys.size > 0 || resolvedSecretNames.size > 0) {
37945
+ container.warnings = container.warnings.filter((w) => {
37946
+ for (const k of resolvedEnvKeys) if (w.startsWith(`Environment '${k}' dropped:`)) return false;
37947
+ for (const k of resolvedSecretNames) if (w.startsWith(`Secret '${k}' dropped:`)) return false;
37948
+ return true;
37949
+ });
37950
+ task.warnings = task.warnings.filter((w) => {
37951
+ for (const k of resolvedEnvKeys) if (w.startsWith(`Container '${containerName}': Environment '${k}' dropped:`)) return false;
37952
+ for (const k of resolvedSecretNames) if (w.startsWith(`Container '${containerName}': Secret '${k}' dropped:`)) return false;
37953
+ return true;
37954
+ });
37955
+ }
37956
+ }
37957
+ }
37592
37958
 
37593
37959
  //#endregion
37594
37960
  //#region src/local/runtime-image.ts
@@ -44605,8 +44971,34 @@ async function localRunTaskCommand(target, options) {
44605
44971
  ...Object.keys(context).length > 0 && { context }
44606
44972
  };
44607
44973
  const { stacks } = await synthesizer.synthesize(synthOpts);
44608
- const task = resolveEcsTaskTarget(target, stacks, await buildEcsImageResolutionContext(target, stacks, options));
44974
+ const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
44975
+ const task = resolveEcsTaskTarget(target, stacks, imageContext);
44609
44976
  logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
44977
+ const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
44978
+ let taskCrossStackDispose;
44979
+ if (options.fromState && taskNeeds.needsCrossStackResolver) {
44980
+ const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
44981
+ const built = await buildCrossStackResolver(consumerRegion, {
44982
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
44983
+ statePrefix: options.statePrefix,
44984
+ ...options.region !== void 0 && { region: options.region },
44985
+ ...options.profile !== void 0 && { profile: options.profile }
44986
+ });
44987
+ if (built) {
44988
+ taskCrossStackDispose = built.dispose;
44989
+ try {
44990
+ await applyCrossStackResolverToTask(task, {
44991
+ resources: imageContext?.stateResources ?? {},
44992
+ ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
44993
+ consumerRegion,
44994
+ crossStackResolver: built.resolver
44995
+ });
44996
+ } finally {
44997
+ taskCrossStackDispose();
44998
+ taskCrossStackDispose = void 0;
44999
+ }
45000
+ }
45001
+ } else if (!options.fromState && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state to substitute them against deployed cdkd state.");
44610
45002
  sigintHandler = () => {
44611
45003
  sigintCount += 1;
44612
45004
  if (sigintCount >= 2) {
@@ -44894,6 +45286,7 @@ async function localInvokeCommand(target, options) {
44894
45286
  let stateAudit;
44895
45287
  let templateEnv = getTemplateEnv(lambda.resource);
44896
45288
  let stateForRoleHint;
45289
+ let crossStackDispose;
44897
45290
  if (options.fromState) {
44898
45291
  const loaded = await loadStateForStack(lambda.stack.stackName, lambda.stack.region, {
44899
45292
  ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
@@ -44904,16 +45297,38 @@ async function localInvokeCommand(target, options) {
44904
45297
  });
44905
45298
  if (loaded) {
44906
45299
  stateForRoleHint = loaded.state;
44907
- const subContext = { resources: loaded.state.resources };
45300
+ const subContext = {
45301
+ resources: loaded.state.resources,
45302
+ consumerRegion: loaded.region
45303
+ };
44908
45304
  if (envHasIntrinsicValue(templateEnv)) {
44909
45305
  const pseudo = await resolvePseudoParametersForInvoke(lambda.stack.region, options);
44910
45306
  if (pseudo) subContext.pseudoParameters = pseudo;
44911
45307
  }
44912
- const { env, audit } = substituteEnvVarsFromState(templateEnv, subContext);
44913
- templateEnv = env;
44914
- stateAudit = audit;
44915
- for (const key of audit.resolvedKeys) logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
44916
- for (const { key, reason } of audit.unresolved) logger.warn(`--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
45308
+ if (envHasCrossStackIntrinsic(templateEnv)) {
45309
+ const built = await buildCrossStackResolver(loaded.region, {
45310
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
45311
+ statePrefix: options.statePrefix,
45312
+ ...options.region !== void 0 && { region: options.region },
45313
+ ...options.profile !== void 0 && { profile: options.profile }
45314
+ });
45315
+ if (built) {
45316
+ subContext.crossStackResolver = built.resolver;
45317
+ crossStackDispose = built.dispose;
45318
+ }
45319
+ }
45320
+ try {
45321
+ const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
45322
+ templateEnv = env;
45323
+ stateAudit = audit;
45324
+ for (const key of audit.resolvedKeys) logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
45325
+ for (const { key, reason } of audit.unresolved) logger.warn(`--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
45326
+ } finally {
45327
+ if (crossStackDispose) {
45328
+ crossStackDispose();
45329
+ crossStackDispose = void 0;
45330
+ }
45331
+ }
44917
45332
  }
44918
45333
  }
44919
45334
  const overrides = readEnvOverridesFile(options.envVars);
@@ -45186,6 +45601,29 @@ function envHasIntrinsicValue(templateEnv) {
45186
45601
  return false;
45187
45602
  }
45188
45603
  /**
45604
+ * Returns true when any value in the function's template env map carries
45605
+ * a top-level `Fn::ImportValue` / `Fn::GetStackOutput` intrinsic. Used to
45606
+ * gate the cross-stack resolver construction inside the `--from-state`
45607
+ * flow: literal + same-stack-intrinsic env maps shouldn't pay for the
45608
+ * extra S3 client / index-load cost (issue #454).
45609
+ *
45610
+ * Detection is one level deep — same heuristic CDK 2.x uses for these
45611
+ * intrinsics in practice. Nested cross-stack intrinsics (e.g. an
45612
+ * `Fn::ImportValue` buried inside a `Fn::Join`) are not detected here;
45613
+ * those won't resolve in v1 anyway because the async resolver path
45614
+ * defers to the sync helper for `Fn::Join` / `Fn::Sub` bodies (see
45615
+ * `substituteAgainstStateAsync` docstring).
45616
+ */
45617
+ function envHasCrossStackIntrinsic(templateEnv) {
45618
+ if (!templateEnv) return false;
45619
+ for (const v of Object.values(templateEnv)) {
45620
+ if (!v || typeof v !== "object") continue;
45621
+ const obj = v;
45622
+ if ("Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj) return true;
45623
+ }
45624
+ return false;
45625
+ }
45626
+ /**
45189
45627
  * Build the AWS pseudo-parameter bag for `--from-state` env-var
45190
45628
  * substitution. Issues a single `sts:GetCallerIdentity` for the account
45191
45629
  * id and derives `partition` / `urlSuffix` from the resolved region. Any
@@ -46717,7 +47155,7 @@ function reorderArgs(argv) {
46717
47155
  */
46718
47156
  async function main() {
46719
47157
  const program = new Command();
46720
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.121.0");
47158
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.123.0");
46721
47159
  program.addCommand(createBootstrapCommand());
46722
47160
  program.addCommand(createSynthCommand());
46723
47161
  program.addCommand(createListCommand());