@go-to-k/cdkd 0.91.1 → 0.91.3

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
@@ -47837,22 +47837,32 @@ var ECSProvider = class {
47837
47837
  essential: def["Essential"],
47838
47838
  command: def["Command"],
47839
47839
  entryPoint: def["EntryPoint"],
47840
- environment: def["Environment"],
47841
- environmentFiles: def["EnvironmentFiles"],
47842
- secrets: def["Secrets"],
47840
+ environment: this.convertEnvironment(
47841
+ def["Environment"]
47842
+ ),
47843
+ environmentFiles: this.convertEnvironmentFiles(
47844
+ def["EnvironmentFiles"]
47845
+ ),
47846
+ secrets: this.convertSecrets(def["Secrets"]),
47843
47847
  portMappings: this.convertPortMappings(
47844
47848
  def["PortMappings"]
47845
47849
  ),
47846
- mountPoints: def["MountPoints"],
47847
- volumesFrom: def["VolumesFrom"],
47848
- dependsOn: def["DependsOn"],
47850
+ mountPoints: this.convertMountPoints(
47851
+ def["MountPoints"]
47852
+ ),
47853
+ volumesFrom: this.convertVolumesFrom(
47854
+ def["VolumesFrom"]
47855
+ ),
47856
+ dependsOn: this.convertDependsOn(
47857
+ def["DependsOn"]
47858
+ ),
47849
47859
  links: def["Links"],
47850
47860
  workingDirectory: def["WorkingDirectory"],
47851
47861
  disableNetworking: def["DisableNetworking"],
47852
47862
  privileged: def["Privileged"],
47853
47863
  readonlyRootFilesystem: def["ReadonlyRootFilesystem"],
47854
47864
  user: def["User"],
47855
- ulimits: def["Ulimits"],
47865
+ ulimits: this.convertUlimits(def["Ulimits"]),
47856
47866
  logConfiguration: this.convertLogConfiguration(
47857
47867
  def["LogConfiguration"]
47858
47868
  ),
@@ -47881,6 +47891,103 @@ var ECSProvider = class {
47881
47891
  name: m["Name"]
47882
47892
  }));
47883
47893
  }
47894
+ /**
47895
+ * Convert CFn Environment (KeyValuePair) to ECS SDK format.
47896
+ * CFn template emits `{Name, Value}` (PascalCase); ECS SDK requires
47897
+ * `{name, value}` (camelCase). Pre-fix the cast `as KeyValuePair[]`
47898
+ * silently dropped both fields and AWS rejected RegisterTaskDefinition
47899
+ * with a generic null/empty validation error.
47900
+ */
47901
+ convertEnvironment(entries) {
47902
+ if (!entries)
47903
+ return void 0;
47904
+ return entries.map((e) => ({
47905
+ name: e["Name"],
47906
+ value: e["Value"]
47907
+ }));
47908
+ }
47909
+ /**
47910
+ * Convert CFn EnvironmentFiles (S3-backed env-var files) to ECS SDK format.
47911
+ * CFn: `{Type, Value}` → SDK: `{type, value}`.
47912
+ */
47913
+ convertEnvironmentFiles(entries) {
47914
+ if (!entries)
47915
+ return void 0;
47916
+ return entries.map((e) => ({
47917
+ type: e["Type"],
47918
+ value: e["Value"]
47919
+ }));
47920
+ }
47921
+ /**
47922
+ * Convert CFn Secrets to ECS SDK format.
47923
+ * CFn: `{Name, ValueFrom}` → SDK: `{name, valueFrom}`.
47924
+ * Same PascalCase→camelCase trap as convertEnvironment — discovered
47925
+ * end-to-end via the local-run-task-from-state integ on 2026-05-12
47926
+ * (issue #291 fixture).
47927
+ */
47928
+ convertSecrets(entries) {
47929
+ if (!entries)
47930
+ return void 0;
47931
+ return entries.map((e) => ({
47932
+ name: e["Name"],
47933
+ valueFrom: e["ValueFrom"]
47934
+ }));
47935
+ }
47936
+ /**
47937
+ * Convert CFn MountPoints to ECS SDK format.
47938
+ * CFn: `{SourceVolume, ContainerPath, ReadOnly}` → SDK: `{sourceVolume,
47939
+ * containerPath, readOnly}`.
47940
+ */
47941
+ convertMountPoints(entries) {
47942
+ if (!entries)
47943
+ return void 0;
47944
+ return entries.map((e) => ({
47945
+ sourceVolume: e["SourceVolume"],
47946
+ containerPath: e["ContainerPath"],
47947
+ readOnly: e["ReadOnly"]
47948
+ }));
47949
+ }
47950
+ /**
47951
+ * Convert CFn VolumesFrom to ECS SDK format.
47952
+ * CFn: `{SourceContainer, ReadOnly}` → SDK: `{sourceContainer, readOnly}`.
47953
+ */
47954
+ convertVolumesFrom(entries) {
47955
+ if (!entries)
47956
+ return void 0;
47957
+ return entries.map((e) => ({
47958
+ sourceContainer: e["SourceContainer"],
47959
+ readOnly: e["ReadOnly"]
47960
+ }));
47961
+ }
47962
+ /**
47963
+ * Convert CFn DependsOn to ECS SDK format.
47964
+ * CFn: `{ContainerName, Condition}` → SDK: `{containerName, condition}`.
47965
+ * The pre-existing local-run-task-multi-container integ was relying
47966
+ * on ECS SDK being lenient about the dependsOn key casing on input,
47967
+ * but per the SDK type definition the input is camelCase so this
47968
+ * converter brings the actual wire shape in line with the contract.
47969
+ */
47970
+ convertDependsOn(entries) {
47971
+ if (!entries)
47972
+ return void 0;
47973
+ return entries.map((e) => ({
47974
+ containerName: e["ContainerName"],
47975
+ condition: e["Condition"]
47976
+ }));
47977
+ }
47978
+ /**
47979
+ * Convert CFn Ulimits to ECS SDK format.
47980
+ * CFn: `{Name, SoftLimit, HardLimit}` → SDK: `{name, softLimit, hardLimit}`.
47981
+ */
47982
+ convertUlimits(entries) {
47983
+ if (!entries)
47984
+ return void 0;
47985
+ return entries.map((e) => ({
47986
+ name: e["Name"],
47987
+ softLimit: e["SoftLimit"],
47988
+ hardLimit: e["HardLimit"]
47989
+ }));
47990
+ }
47884
47991
  /**
47885
47992
  * Convert CFn LogConfiguration to ECS SDK format
47886
47993
  */
@@ -70168,167 +70275,447 @@ async function loadStateForStack(stackName, synthRegion, opts) {
70168
70275
  // src/local/lambda-resolver.ts
70169
70276
  import { existsSync as existsSync4, statSync as statSync3 } from "node:fs";
70170
70277
  import { dirname, isAbsolute, resolve as resolve4 } from "node:path";
70171
- var LocalInvokeResolutionError = class _LocalInvokeResolutionError extends Error {
70172
- constructor(message) {
70173
- super(message);
70174
- this.name = "LocalInvokeResolutionError";
70175
- Object.setPrototypeOf(this, _LocalInvokeResolutionError.prototype);
70176
- }
70177
- };
70178
- function parseTarget(target) {
70179
- if (typeof target !== "string" || target.length === 0) {
70180
- throw new LocalInvokeResolutionError(
70181
- "Empty target. Pass a CDK display path (e.g. 'MyStack/MyApi/Handler') or stack-qualified logical ID (e.g. 'MyStack:MyApiHandler1234ABCD')."
70182
- );
70183
- }
70184
- const colonIdx = target.indexOf(":");
70185
- const slashIdx = target.indexOf("/");
70186
- if (colonIdx > 0 && (slashIdx === -1 || colonIdx < slashIdx)) {
70187
- const stackPattern = target.substring(0, colonIdx);
70188
- const pathOrId = target.substring(colonIdx + 1);
70189
- if (pathOrId.length === 0) {
70190
- throw new LocalInvokeResolutionError(`Target '${target}' has no logical ID after ':'.`);
70191
- }
70192
- return { stackPattern, pathOrId, isPath: pathOrId.includes("/") };
70193
- }
70194
- if (slashIdx > 0) {
70195
- return { stackPattern: target.substring(0, slashIdx), pathOrId: target, isPath: true };
70278
+
70279
+ // src/local/intrinsic-image.ts
70280
+ function tryResolveImageFnJoin(raw, resources, context) {
70281
+ if (!raw || typeof raw !== "object")
70282
+ return { kind: "not-applicable" };
70283
+ const obj = raw;
70284
+ const arg = obj["Fn::Join"];
70285
+ if (arg === void 0)
70286
+ return { kind: "not-applicable" };
70287
+ if (!Array.isArray(arg) || arg.length !== 2 || !Array.isArray(arg[1])) {
70288
+ return { kind: "unsupported-join", reason: "Fn::Join must be [delimiter, [elements]]" };
70196
70289
  }
70197
- return { stackPattern: null, pathOrId: target, isPath: false };
70198
- }
70199
- function resolveLambdaTarget(target, stacks) {
70200
- if (stacks.length === 0) {
70201
- throw new LocalInvokeResolutionError("No stacks found in the synthesized assembly.");
70290
+ const [delimiter, elements] = arg;
70291
+ if (typeof delimiter !== "string") {
70292
+ return {
70293
+ kind: "unsupported-join",
70294
+ reason: `Fn::Join delimiter must be a string, got ${typeof delimiter}`
70295
+ };
70202
70296
  }
70203
- const parsed = parseTarget(target);
70204
- const stack = pickStack(parsed, stacks);
70205
- const template = stack.template;
70206
- const resources = template.Resources ?? {};
70207
- let match;
70208
- if (parsed.isPath) {
70209
- const index = buildCdkPathIndex(template);
70210
- const resolvedPaths = resolveCdkPathToLogicalIds(parsed.pathOrId, index);
70211
- const lambdaMatches = resolvedPaths.filter(
70212
- ({ logicalId: logicalId2 }) => resources[logicalId2]?.Type === "AWS::Lambda::Function"
70213
- );
70214
- if (lambdaMatches.length === 0) {
70215
- throw notFoundError(target, stack, resources);
70216
- }
70217
- if (lambdaMatches.length > 1) {
70218
- throw new LocalInvokeResolutionError(
70219
- `Target '${target}' matches ${lambdaMatches.length} Lambda functions in ${stack.stackName}: ` + lambdaMatches.map((m2) => m2.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form."
70220
- );
70221
- }
70222
- const m = lambdaMatches[0];
70223
- match = { logicalId: m.logicalId, resource: resources[m.logicalId] };
70224
- } else {
70225
- const resource2 = resources[parsed.pathOrId];
70226
- if (!resource2) {
70227
- throw notFoundError(target, stack, resources);
70228
- }
70229
- match = { logicalId: parsed.pathOrId, resource: resource2 };
70297
+ const repoLogicalId = findEcrRepositoryRefInTree(elements, resources);
70298
+ const stateResources = context?.stateResources;
70299
+ if (repoLogicalId && !stateResources) {
70300
+ return { kind: "needs-state", repoLogicalId };
70230
70301
  }
70231
- const { logicalId, resource } = match;
70232
- if (resource.Type !== "AWS::Lambda::Function") {
70233
- if (resource.Type.startsWith("Custom::")) {
70234
- throw new LocalInvokeResolutionError(
70235
- `Resource '${logicalId}' in ${stack.stackName} is a Custom Resource (${resource.Type}), not a Lambda function. Custom Resources are invoked by the deploy framework, not by users. If you want to test the underlying handler, target the ServiceToken Lambda directly.`
70236
- );
70302
+ const parts = [];
70303
+ for (const element of elements) {
70304
+ const r = resolveImageIntrinsic(element, resources, context);
70305
+ if (r === void 0) {
70306
+ if (!repoLogicalId)
70307
+ return { kind: "not-applicable" };
70308
+ return {
70309
+ kind: "unsupported-join",
70310
+ reason: "one or more Fn::Join elements could not be resolved"
70311
+ };
70237
70312
  }
70238
- throw new LocalInvokeResolutionError(
70239
- `Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not a Lambda function. cdkd local invoke only works on AWS::Lambda::Function resources in v1.`
70240
- );
70313
+ parts.push(r);
70241
70314
  }
70242
- return extractLambdaProperties(stack, logicalId, resource);
70315
+ return { kind: "resolved", uri: parts.join(delimiter) };
70243
70316
  }
70244
- function pickStack(parsed, stacks) {
70245
- if (parsed.stackPattern === null) {
70246
- if (stacks.length === 1)
70247
- return stacks[0];
70248
- throw new LocalInvokeResolutionError(
70249
- `Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`
70250
- );
70251
- }
70252
- const matched = matchStacks(stacks, [parsed.stackPattern]);
70253
- if (matched.length === 0) {
70254
- throw new LocalInvokeResolutionError(
70255
- `Stack '${parsed.stackPattern}' not found. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`
70256
- );
70257
- }
70258
- if (matched.length > 1) {
70259
- throw new LocalInvokeResolutionError(
70260
- `Stack pattern '${parsed.stackPattern}' matched ${matched.length} stacks: ` + matched.map((s) => s.stackName).join(", ") + ". Use a more specific pattern."
70261
- );
70317
+ function findEcrRepositoryRefInTree(node, resources) {
70318
+ if (node === null || node === void 0)
70319
+ return void 0;
70320
+ if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
70321
+ return void 0;
70262
70322
  }
70263
- return matched[0];
70264
- }
70265
- function extractLambdaProperties(stack, logicalId, resource) {
70266
- const props = resource.Properties ?? {};
70267
- const memoryMb = typeof props["MemorySize"] === "number" ? props["MemorySize"] : 128;
70268
- const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
70269
- const code = props["Code"] ?? {};
70270
- const imageUri = extractImageUri(code["ImageUri"]);
70271
- if (imageUri !== void 0) {
70272
- return extractImageLambdaProperties({
70273
- stack,
70274
- logicalId,
70275
- resource,
70276
- memoryMb,
70277
- timeoutSec,
70278
- props,
70279
- imageUri
70280
- });
70323
+ if (Array.isArray(node)) {
70324
+ for (const item of node) {
70325
+ const hit = findEcrRepositoryRefInTree(item, resources);
70326
+ if (hit)
70327
+ return hit;
70328
+ }
70329
+ return void 0;
70281
70330
  }
70282
- const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
70283
- const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
70284
- if (!runtime) {
70285
- throw new LocalInvokeResolutionError(
70286
- `Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. cdkd cannot tell if this is a ZIP or a container Lambda.`
70287
- );
70331
+ if (typeof node !== "object")
70332
+ return void 0;
70333
+ const obj = node;
70334
+ if (typeof obj["Ref"] === "string") {
70335
+ const target = obj["Ref"];
70336
+ if (resources[target]?.Type === "AWS::ECR::Repository")
70337
+ return target;
70338
+ return void 0;
70288
70339
  }
70289
- if (!handler) {
70290
- throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Handler property.`);
70340
+ const getAtt = obj["Fn::GetAtt"];
70341
+ if (getAtt !== void 0) {
70342
+ let lid;
70343
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string")
70344
+ lid = getAtt[0];
70345
+ else if (typeof getAtt === "string")
70346
+ lid = getAtt.split(".")[0];
70347
+ if (lid && resources[lid]?.Type === "AWS::ECR::Repository")
70348
+ return lid;
70349
+ return void 0;
70291
70350
  }
70292
- const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
70293
- let codePath = null;
70294
- if (!inlineCode) {
70295
- codePath = resolveAssetCodePath(stack, logicalId, resource);
70351
+ for (const value of Object.values(obj)) {
70352
+ const hit = findEcrRepositoryRefInTree(value, resources);
70353
+ if (hit)
70354
+ return hit;
70296
70355
  }
70297
- const layers = resolveLambdaLayers(stack, logicalId, props);
70298
- return {
70299
- kind: "zip",
70300
- stack,
70301
- logicalId,
70302
- resource,
70303
- runtime,
70304
- handler,
70305
- memoryMb,
70306
- timeoutSec,
70307
- codePath,
70308
- layers,
70309
- ...inlineCode !== void 0 && { inlineCode }
70310
- };
70356
+ return void 0;
70311
70357
  }
70312
- function extractImageUri(value) {
70313
- if (typeof value === "string" && value.length > 0)
70314
- return value;
70315
- if (value && typeof value === "object" && !Array.isArray(value)) {
70316
- const obj = value;
70317
- const sub = obj["Fn::Sub"];
70318
- if (typeof sub === "string" && sub.length > 0)
70319
- return sub;
70320
- if (Array.isArray(sub) && typeof sub[0] === "string")
70321
- return sub[0];
70322
- }
70358
+ function resolveImageIntrinsic(node, resources, context) {
70359
+ const v = resolveImageIntrinsicAny(node, resources, context);
70360
+ if (typeof v === "string")
70361
+ return v;
70362
+ if (typeof v === "number" || typeof v === "boolean")
70363
+ return String(v);
70323
70364
  return void 0;
70324
70365
  }
70325
- function extractImageLambdaProperties(args) {
70326
- const { stack, logicalId, resource, memoryMb, timeoutSec, props, imageUri } = args;
70327
- const rawImageConfig = props["ImageConfig"] ?? {};
70328
- const imageConfig = {};
70329
- if (Array.isArray(rawImageConfig["Command"])) {
70330
- imageConfig.command = rawImageConfig["Command"].filter(
70331
- (s) => typeof s === "string"
70366
+ function resolveImageIntrinsicAny(node, resources, context) {
70367
+ if (node === null || node === void 0)
70368
+ return void 0;
70369
+ if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
70370
+ return node;
70371
+ }
70372
+ if (Array.isArray(node)) {
70373
+ return void 0;
70374
+ }
70375
+ if (typeof node !== "object")
70376
+ return void 0;
70377
+ const obj = node;
70378
+ const keys = Object.keys(obj);
70379
+ if (keys.length !== 1)
70380
+ return void 0;
70381
+ const intrinsic = keys[0];
70382
+ const arg = obj[intrinsic];
70383
+ if (intrinsic === "Ref") {
70384
+ if (typeof arg !== "string")
70385
+ return void 0;
70386
+ if (arg.startsWith("AWS::")) {
70387
+ const p = context?.pseudoParameters;
70388
+ if (!p)
70389
+ return void 0;
70390
+ if (arg === "AWS::URLSuffix")
70391
+ return p.urlSuffix;
70392
+ if (arg === "AWS::Partition")
70393
+ return p.partition;
70394
+ if (arg === "AWS::Region")
70395
+ return p.region;
70396
+ if (arg === "AWS::AccountId")
70397
+ return p.accountId;
70398
+ return void 0;
70399
+ }
70400
+ const refResource = resources[arg];
70401
+ if (refResource?.Type !== "AWS::ECR::Repository")
70402
+ return void 0;
70403
+ const stateEntry = context?.stateResources?.[arg];
70404
+ if (!stateEntry)
70405
+ return void 0;
70406
+ return stateEntry.physicalId;
70407
+ }
70408
+ if (intrinsic === "Fn::GetAtt") {
70409
+ let logicalId;
70410
+ let attr;
70411
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
70412
+ logicalId = arg[0];
70413
+ attr = arg[1];
70414
+ } else if (typeof arg === "string") {
70415
+ const dot = arg.indexOf(".");
70416
+ if (dot > 0 && dot < arg.length - 1) {
70417
+ logicalId = arg.slice(0, dot);
70418
+ attr = arg.slice(dot + 1);
70419
+ }
70420
+ }
70421
+ if (!logicalId || !attr)
70422
+ return void 0;
70423
+ if (resources[logicalId]?.Type !== "AWS::ECR::Repository")
70424
+ return void 0;
70425
+ const cached = context?.stateResources?.[logicalId]?.attributes?.[attr];
70426
+ if (typeof cached === "string" && cached.length > 0)
70427
+ return cached;
70428
+ return void 0;
70429
+ }
70430
+ if (intrinsic === "Fn::Split") {
70431
+ if (!Array.isArray(arg) || arg.length !== 2)
70432
+ return void 0;
70433
+ const argArr = arg;
70434
+ const delim = argArr[0];
70435
+ if (typeof delim !== "string")
70436
+ return void 0;
70437
+ const src = resolveImageIntrinsicAny(argArr[1], resources, context);
70438
+ if (typeof src !== "string")
70439
+ return void 0;
70440
+ return src.split(delim);
70441
+ }
70442
+ if (intrinsic === "Fn::Select") {
70443
+ if (!Array.isArray(arg) || arg.length !== 2)
70444
+ return void 0;
70445
+ const argArr = arg;
70446
+ const rawIndex = argArr[0];
70447
+ let index;
70448
+ if (typeof rawIndex === "number") {
70449
+ index = rawIndex;
70450
+ } else if (typeof rawIndex === "string" && /^-?\d+$/.test(rawIndex)) {
70451
+ index = Number.parseInt(rawIndex, 10);
70452
+ }
70453
+ if (index === void 0 || !Number.isFinite(index))
70454
+ return void 0;
70455
+ const list = resolveImageIntrinsicAny(argArr[1], resources, context);
70456
+ if (Array.isArray(list)) {
70457
+ if (index < 0 || index >= list.length)
70458
+ return void 0;
70459
+ const picked = list[index];
70460
+ if (typeof picked === "string")
70461
+ return picked;
70462
+ return void 0;
70463
+ }
70464
+ if (Array.isArray(argArr[1])) {
70465
+ const listLiteral = argArr[1];
70466
+ if (index < 0 || index >= listLiteral.length)
70467
+ return void 0;
70468
+ return resolveImageIntrinsic(listLiteral[index], resources, context);
70469
+ }
70470
+ return void 0;
70471
+ }
70472
+ if (intrinsic === "Fn::Join") {
70473
+ if (!Array.isArray(arg) || arg.length !== 2)
70474
+ return void 0;
70475
+ const [delim, parts] = arg;
70476
+ if (typeof delim !== "string" || !Array.isArray(parts))
70477
+ return void 0;
70478
+ const resolved = [];
70479
+ for (const part of parts) {
70480
+ const r = resolveImageIntrinsic(part, resources, context);
70481
+ if (r === void 0)
70482
+ return void 0;
70483
+ resolved.push(r);
70484
+ }
70485
+ return resolved.join(delim);
70486
+ }
70487
+ if (intrinsic === "Fn::Sub") {
70488
+ let template;
70489
+ if (typeof arg === "string")
70490
+ template = arg;
70491
+ else if (Array.isArray(arg) && typeof arg[0] === "string")
70492
+ template = arg[0];
70493
+ if (template === void 0)
70494
+ return void 0;
70495
+ const out = substituteImagePlaceholders(template, resources, context);
70496
+ if (out.includes("${"))
70497
+ return void 0;
70498
+ return out;
70499
+ }
70500
+ return void 0;
70501
+ }
70502
+ function substituteImagePlaceholders(flat, resources, context) {
70503
+ if (!flat.includes("${"))
70504
+ return flat;
70505
+ return flat.replace(/\$\{([^}]+)\}/g, (full, key) => {
70506
+ if (context?.pseudoParameters) {
70507
+ if (key === "AWS::AccountId" && context.pseudoParameters.accountId) {
70508
+ return context.pseudoParameters.accountId;
70509
+ }
70510
+ if (key === "AWS::Region" && context.pseudoParameters.region) {
70511
+ return context.pseudoParameters.region;
70512
+ }
70513
+ if (key === "AWS::Partition" && context.pseudoParameters.partition) {
70514
+ return context.pseudoParameters.partition;
70515
+ }
70516
+ if (key === "AWS::URLSuffix" && context.pseudoParameters.urlSuffix) {
70517
+ return context.pseudoParameters.urlSuffix;
70518
+ }
70519
+ }
70520
+ if (context?.stateResources) {
70521
+ const dot = key.indexOf(".");
70522
+ const logicalId = dot === -1 ? key : key.slice(0, dot);
70523
+ const refResource = resources[logicalId];
70524
+ const stateEntry = context.stateResources[logicalId];
70525
+ if (refResource?.Type === "AWS::ECR::Repository" && stateEntry) {
70526
+ if (dot === -1) {
70527
+ return stateEntry.physicalId;
70528
+ }
70529
+ const attr = key.slice(dot + 1);
70530
+ const cached = stateEntry.attributes?.[attr];
70531
+ if (typeof cached === "string")
70532
+ return cached;
70533
+ }
70534
+ }
70535
+ return full;
70536
+ });
70537
+ }
70538
+
70539
+ // src/local/lambda-resolver.ts
70540
+ var LocalInvokeResolutionError = class _LocalInvokeResolutionError extends Error {
70541
+ constructor(message) {
70542
+ super(message);
70543
+ this.name = "LocalInvokeResolutionError";
70544
+ Object.setPrototypeOf(this, _LocalInvokeResolutionError.prototype);
70545
+ }
70546
+ };
70547
+ function parseTarget(target) {
70548
+ if (typeof target !== "string" || target.length === 0) {
70549
+ throw new LocalInvokeResolutionError(
70550
+ "Empty target. Pass a CDK display path (e.g. 'MyStack/MyApi/Handler') or stack-qualified logical ID (e.g. 'MyStack:MyApiHandler1234ABCD')."
70551
+ );
70552
+ }
70553
+ const colonIdx = target.indexOf(":");
70554
+ const slashIdx = target.indexOf("/");
70555
+ if (colonIdx > 0 && (slashIdx === -1 || colonIdx < slashIdx)) {
70556
+ const stackPattern = target.substring(0, colonIdx);
70557
+ const pathOrId = target.substring(colonIdx + 1);
70558
+ if (pathOrId.length === 0) {
70559
+ throw new LocalInvokeResolutionError(`Target '${target}' has no logical ID after ':'.`);
70560
+ }
70561
+ return { stackPattern, pathOrId, isPath: pathOrId.includes("/") };
70562
+ }
70563
+ if (slashIdx > 0) {
70564
+ return { stackPattern: target.substring(0, slashIdx), pathOrId: target, isPath: true };
70565
+ }
70566
+ return { stackPattern: null, pathOrId: target, isPath: false };
70567
+ }
70568
+ function resolveLambdaTarget(target, stacks) {
70569
+ if (stacks.length === 0) {
70570
+ throw new LocalInvokeResolutionError("No stacks found in the synthesized assembly.");
70571
+ }
70572
+ const parsed = parseTarget(target);
70573
+ const stack = pickStack(parsed, stacks);
70574
+ const template = stack.template;
70575
+ const resources = template.Resources ?? {};
70576
+ let match;
70577
+ if (parsed.isPath) {
70578
+ const index = buildCdkPathIndex(template);
70579
+ const resolvedPaths = resolveCdkPathToLogicalIds(parsed.pathOrId, index);
70580
+ const lambdaMatches = resolvedPaths.filter(
70581
+ ({ logicalId: logicalId2 }) => resources[logicalId2]?.Type === "AWS::Lambda::Function"
70582
+ );
70583
+ if (lambdaMatches.length === 0) {
70584
+ throw notFoundError(target, stack, resources);
70585
+ }
70586
+ if (lambdaMatches.length > 1) {
70587
+ throw new LocalInvokeResolutionError(
70588
+ `Target '${target}' matches ${lambdaMatches.length} Lambda functions in ${stack.stackName}: ` + lambdaMatches.map((m2) => m2.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form."
70589
+ );
70590
+ }
70591
+ const m = lambdaMatches[0];
70592
+ match = { logicalId: m.logicalId, resource: resources[m.logicalId] };
70593
+ } else {
70594
+ const resource2 = resources[parsed.pathOrId];
70595
+ if (!resource2) {
70596
+ throw notFoundError(target, stack, resources);
70597
+ }
70598
+ match = { logicalId: parsed.pathOrId, resource: resource2 };
70599
+ }
70600
+ const { logicalId, resource } = match;
70601
+ if (resource.Type !== "AWS::Lambda::Function") {
70602
+ if (resource.Type.startsWith("Custom::")) {
70603
+ throw new LocalInvokeResolutionError(
70604
+ `Resource '${logicalId}' in ${stack.stackName} is a Custom Resource (${resource.Type}), not a Lambda function. Custom Resources are invoked by the deploy framework, not by users. If you want to test the underlying handler, target the ServiceToken Lambda directly.`
70605
+ );
70606
+ }
70607
+ throw new LocalInvokeResolutionError(
70608
+ `Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not a Lambda function. cdkd local invoke only works on AWS::Lambda::Function resources in v1.`
70609
+ );
70610
+ }
70611
+ return extractLambdaProperties(stack, logicalId, resource, resources);
70612
+ }
70613
+ function pickStack(parsed, stacks) {
70614
+ if (parsed.stackPattern === null) {
70615
+ if (stacks.length === 1)
70616
+ return stacks[0];
70617
+ throw new LocalInvokeResolutionError(
70618
+ `Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`
70619
+ );
70620
+ }
70621
+ const matched = matchStacks(stacks, [parsed.stackPattern]);
70622
+ if (matched.length === 0) {
70623
+ throw new LocalInvokeResolutionError(
70624
+ `Stack '${parsed.stackPattern}' not found. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`
70625
+ );
70626
+ }
70627
+ if (matched.length > 1) {
70628
+ throw new LocalInvokeResolutionError(
70629
+ `Stack pattern '${parsed.stackPattern}' matched ${matched.length} stacks: ` + matched.map((s) => s.stackName).join(", ") + ". Use a more specific pattern."
70630
+ );
70631
+ }
70632
+ return matched[0];
70633
+ }
70634
+ function extractLambdaProperties(stack, logicalId, resource, resources) {
70635
+ const props = resource.Properties ?? {};
70636
+ const memoryMb = typeof props["MemorySize"] === "number" ? props["MemorySize"] : 128;
70637
+ const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
70638
+ const code = props["Code"] ?? {};
70639
+ const imageUri = extractImageUri(code["ImageUri"], logicalId, stack.stackName, resources);
70640
+ if (imageUri !== void 0) {
70641
+ return extractImageLambdaProperties({
70642
+ stack,
70643
+ logicalId,
70644
+ resource,
70645
+ memoryMb,
70646
+ timeoutSec,
70647
+ props,
70648
+ imageUri
70649
+ });
70650
+ }
70651
+ const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
70652
+ const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
70653
+ if (!runtime) {
70654
+ throw new LocalInvokeResolutionError(
70655
+ `Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. cdkd cannot tell if this is a ZIP or a container Lambda.`
70656
+ );
70657
+ }
70658
+ if (!handler) {
70659
+ throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Handler property.`);
70660
+ }
70661
+ const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
70662
+ let codePath = null;
70663
+ if (!inlineCode) {
70664
+ codePath = resolveAssetCodePath(stack, logicalId, resource);
70665
+ }
70666
+ const layers = resolveLambdaLayers(stack, logicalId, props);
70667
+ return {
70668
+ kind: "zip",
70669
+ stack,
70670
+ logicalId,
70671
+ resource,
70672
+ runtime,
70673
+ handler,
70674
+ memoryMb,
70675
+ timeoutSec,
70676
+ codePath,
70677
+ layers,
70678
+ ...inlineCode !== void 0 && { inlineCode }
70679
+ };
70680
+ }
70681
+ function extractImageUri(value, logicalId, stackName, resources) {
70682
+ if (typeof value === "string" && value.length > 0)
70683
+ return value;
70684
+ if (value && typeof value === "object" && !Array.isArray(value)) {
70685
+ const obj = value;
70686
+ const sub = obj["Fn::Sub"];
70687
+ if (typeof sub === "string" && sub.length > 0)
70688
+ return sub;
70689
+ if (Array.isArray(sub) && typeof sub[0] === "string")
70690
+ return sub[0];
70691
+ if ("Fn::Join" in obj) {
70692
+ const joinResolved = tryResolveImageFnJoin(value, resources, void 0);
70693
+ if (joinResolved.kind === "resolved")
70694
+ return joinResolved.uri;
70695
+ if (joinResolved.kind === "needs-state") {
70696
+ throw new LocalInvokeResolutionError(
70697
+ `Lambda '${logicalId}' in ${stackName} references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join. cdkd local invoke cannot resolve the repository URI without state \u2014 deploy the stack first (so cdkd records the repository physical id), rebuild via lambda.DockerImageCode.fromImageAsset, or pin a public image.`
70698
+ );
70699
+ }
70700
+ if (joinResolved.kind === "unsupported-join") {
70701
+ throw new LocalInvokeResolutionError(
70702
+ `Lambda '${logicalId}' in ${stackName} has an unsupported Fn::Join Code.ImageUri shape: ${joinResolved.reason}. cdkd local invoke recognizes the canonical CDK 2.x lambda.DockerImageCode.fromEcr Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`
70703
+ );
70704
+ }
70705
+ throw new LocalInvokeResolutionError(
70706
+ `Lambda '${logicalId}' in ${stackName} has an Fn::Join Code.ImageUri that cdkd local invoke cannot resolve. The shape likely references AWS pseudo parameters (e.g. \${AWS::URLSuffix}) for an imported ECR repository. Workarounds: rebuild via lambda.DockerImageCode.fromImageAsset, or pin a fully-literal public image URI.`
70707
+ );
70708
+ }
70709
+ }
70710
+ return void 0;
70711
+ }
70712
+ function extractImageLambdaProperties(args) {
70713
+ const { stack, logicalId, resource, memoryMb, timeoutSec, props, imageUri } = args;
70714
+ const rawImageConfig = props["ImageConfig"] ?? {};
70715
+ const imageConfig = {};
70716
+ if (Array.isArray(rawImageConfig["Command"])) {
70717
+ imageConfig.command = rawImageConfig["Command"].filter(
70718
+ (s) => typeof s === "string"
70332
70719
  );
70333
70720
  }
70334
70721
  if (Array.isArray(rawImageConfig["EntryPoint"])) {
@@ -70511,7 +70898,8 @@ function isLiteralEnvValue(value) {
70511
70898
  }
70512
70899
 
70513
70900
  // src/local/state-resolver.ts
70514
- function substituteAgainstState(value, resources) {
70901
+ function substituteAgainstState(value, contextOrResources) {
70902
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
70515
70903
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
70516
70904
  return { kind: "literal", value };
70517
70905
  }
@@ -70532,24 +70920,40 @@ function substituteAgainstState(value, resources) {
70532
70920
  const intrinsic = keys[0];
70533
70921
  const arg = obj[intrinsic];
70534
70922
  if (intrinsic === "Ref") {
70535
- return resolveRef(arg, resources);
70923
+ return resolveRef(arg, context);
70536
70924
  }
70537
70925
  if (intrinsic === "Fn::GetAtt") {
70538
- return resolveGetAtt(arg, resources);
70926
+ return resolveGetAtt(arg, context);
70539
70927
  }
70540
70928
  if (intrinsic === "Fn::Sub") {
70541
- return resolveSub(arg, resources);
70929
+ return resolveSub(arg, context);
70930
+ }
70931
+ if (intrinsic === "Fn::Join") {
70932
+ return resolveJoin(arg, context);
70542
70933
  }
70543
70934
  return {
70544
70935
  kind: "unresolved",
70545
- reason: `unsupported intrinsic '${intrinsic}' (only Ref, Fn::GetAtt, Fn::Sub are wired in --from-state v1)`
70936
+ reason: `unsupported intrinsic '${intrinsic}' (supported: Ref, Fn::GetAtt, Fn::Sub, Fn::Join)`
70546
70937
  };
70547
70938
  }
70548
- function resolveRef(arg, resources) {
70939
+ function isContext(v) {
70940
+ if (typeof v !== "object" || v === null)
70941
+ return false;
70942
+ const r = v["resources"];
70943
+ if (r === void 0)
70944
+ return false;
70945
+ if (typeof r !== "object" || r === null)
70946
+ return false;
70947
+ return !("physicalId" in r);
70948
+ }
70949
+ function resolveRef(arg, context) {
70549
70950
  if (typeof arg !== "string" || arg.length === 0) {
70550
70951
  return { kind: "unresolved", reason: `Ref expects a non-empty logical ID, got ${typeof arg}` };
70551
70952
  }
70552
- const resource = resources[arg];
70953
+ if (arg.startsWith("AWS::")) {
70954
+ return resolvePseudoParameter(arg, context.pseudoParameters);
70955
+ }
70956
+ const resource = context.resources[arg];
70553
70957
  if (!resource) {
70554
70958
  return {
70555
70959
  kind: "unresolved",
@@ -70558,7 +70962,39 @@ function resolveRef(arg, resources) {
70558
70962
  }
70559
70963
  return { kind: "literal", value: resource.physicalId };
70560
70964
  }
70561
- function resolveGetAtt(arg, resources) {
70965
+ function resolvePseudoParameter(name, pseudo) {
70966
+ if (!pseudo) {
70967
+ return {
70968
+ kind: "unresolved",
70969
+ reason: `Ref '${name}': pseudo parameter not supplied (need --from-state context)`
70970
+ };
70971
+ }
70972
+ switch (name) {
70973
+ case "AWS::AccountId":
70974
+ if (pseudo.accountId !== void 0)
70975
+ return { kind: "literal", value: pseudo.accountId };
70976
+ break;
70977
+ case "AWS::Region":
70978
+ if (pseudo.region !== void 0)
70979
+ return { kind: "literal", value: pseudo.region };
70980
+ break;
70981
+ case "AWS::Partition":
70982
+ if (pseudo.partition !== void 0)
70983
+ return { kind: "literal", value: pseudo.partition };
70984
+ break;
70985
+ case "AWS::URLSuffix":
70986
+ if (pseudo.urlSuffix !== void 0)
70987
+ return { kind: "literal", value: pseudo.urlSuffix };
70988
+ break;
70989
+ default:
70990
+ return {
70991
+ kind: "unresolved",
70992
+ reason: `Ref '${name}': pseudo parameter not supported (supported: AWS::AccountId, AWS::Region, AWS::Partition, AWS::URLSuffix)`
70993
+ };
70994
+ }
70995
+ return { kind: "unresolved", reason: `Ref '${name}': pseudo parameter value not resolved` };
70996
+ }
70997
+ function resolveGetAtt(arg, context) {
70562
70998
  let logicalId;
70563
70999
  let attr;
70564
71000
  if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string") {
@@ -70566,7 +71002,7 @@ function resolveGetAtt(arg, resources) {
70566
71002
  if (typeof arg[1] !== "string") {
70567
71003
  return {
70568
71004
  kind: "unresolved",
70569
- reason: `Fn::GetAtt's second arg must be a string attribute name, got ${typeof arg[1]} (nested intrinsics in attribute names are not supported in --from-state v1)`
71005
+ reason: `Fn::GetAtt's second arg must be a string attribute name, got ${typeof arg[1]} (nested intrinsics in attribute names are not supported)`
70570
71006
  };
70571
71007
  }
70572
71008
  attr = arg[1];
@@ -70586,7 +71022,7 @@ function resolveGetAtt(arg, resources) {
70586
71022
  reason: `Fn::GetAtt expects [LogicalId, Attribute] or 'LogicalId.Attribute', got ${Array.isArray(arg) ? `array of length ${arg.length}` : typeof arg}`
70587
71023
  };
70588
71024
  }
70589
- const resource = resources[logicalId];
71025
+ const resource = context.resources[logicalId];
70590
71026
  if (!resource) {
70591
71027
  return {
70592
71028
  kind: "unresolved",
@@ -70605,7 +71041,7 @@ function resolveGetAtt(arg, resources) {
70605
71041
  }
70606
71042
  return { kind: "literal", value: JSON.stringify(cached) };
70607
71043
  }
70608
- function resolveSub(arg, resources) {
71044
+ function resolveSub(arg, context) {
70609
71045
  let template;
70610
71046
  let bindings = {};
70611
71047
  if (typeof arg === "string") {
@@ -70630,7 +71066,18 @@ function resolveSub(arg, resources) {
70630
71066
  if (resolutions.has(placeholder))
70631
71067
  continue;
70632
71068
  if (placeholder in bindings) {
70633
- const sub = substituteAgainstState(bindings[placeholder], resources);
71069
+ const sub = substituteAgainstState(bindings[placeholder], context);
71070
+ if (sub.kind !== "literal") {
71071
+ return {
71072
+ kind: "unresolved",
71073
+ reason: `Fn::Sub placeholder '\${${placeholder}}': ${sub.reason}`
71074
+ };
71075
+ }
71076
+ resolutions.set(placeholder, String(sub.value));
71077
+ continue;
71078
+ }
71079
+ if (placeholder.startsWith("AWS::")) {
71080
+ const sub = resolvePseudoParameter(placeholder, context.pseudoParameters);
70634
71081
  if (sub.kind !== "literal") {
70635
71082
  return {
70636
71083
  kind: "unresolved",
@@ -70642,7 +71089,7 @@ function resolveSub(arg, resources) {
70642
71089
  }
70643
71090
  const dot = placeholder.indexOf(".");
70644
71091
  if (dot === -1) {
70645
- const sub = resolveRef(placeholder, resources);
71092
+ const sub = resolveRef(placeholder, context);
70646
71093
  if (sub.kind !== "literal") {
70647
71094
  return {
70648
71095
  kind: "unresolved",
@@ -70651,7 +71098,7 @@ function resolveSub(arg, resources) {
70651
71098
  }
70652
71099
  resolutions.set(placeholder, String(sub.value));
70653
71100
  } else {
70654
- const sub = resolveGetAtt(placeholder, resources);
71101
+ const sub = resolveGetAtt(placeholder, context);
70655
71102
  if (sub.kind !== "literal") {
70656
71103
  return {
70657
71104
  kind: "unresolved",
@@ -70666,17 +71113,45 @@ function resolveSub(arg, resources) {
70666
71113
  });
70667
71114
  return { kind: "literal", value: out };
70668
71115
  }
70669
- function substituteEnvVarsFromState(templateEnv, resources) {
71116
+ function resolveJoin(arg, context) {
71117
+ if (!Array.isArray(arg) || arg.length !== 2 || !Array.isArray(arg[1])) {
71118
+ return {
71119
+ kind: "unresolved",
71120
+ reason: `Fn::Join expects [delimiter, [elements]], got ${Array.isArray(arg) ? `array of length ${arg.length}` : typeof arg}`
71121
+ };
71122
+ }
71123
+ const [delimiterRaw, elements] = arg;
71124
+ if (typeof delimiterRaw !== "string") {
71125
+ return {
71126
+ kind: "unresolved",
71127
+ reason: `Fn::Join delimiter must be a string, got ${typeof delimiterRaw}`
71128
+ };
71129
+ }
71130
+ const parts = [];
71131
+ for (let i = 0; i < elements.length; i += 1) {
71132
+ const sub = substituteAgainstState(elements[i], context);
71133
+ if (sub.kind !== "literal") {
71134
+ return {
71135
+ kind: "unresolved",
71136
+ reason: `Fn::Join element [${i}]: ${sub.reason}`
71137
+ };
71138
+ }
71139
+ parts.push(String(sub.value));
71140
+ }
71141
+ return { kind: "literal", value: parts.join(delimiterRaw) };
71142
+ }
71143
+ function substituteEnvVarsFromState(templateEnv, contextOrResources) {
70670
71144
  const env = {};
70671
71145
  const audit = { resolvedKeys: [], unresolved: [] };
70672
71146
  if (!templateEnv)
70673
71147
  return { env, audit };
71148
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
70674
71149
  for (const [key, value] of Object.entries(templateEnv)) {
70675
71150
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
70676
71151
  env[key] = value;
70677
71152
  continue;
70678
71153
  }
70679
- const result = substituteAgainstState(value, resources);
71154
+ const result = substituteAgainstState(value, context);
70680
71155
  if (result.kind === "literal") {
70681
71156
  env[key] = result.value;
70682
71157
  audit.resolvedKeys.push(key);
@@ -76783,6 +77258,7 @@ function detectEcsImageResolutionNeeds(stack) {
76783
77258
  const resources = stack.template.Resources ?? {};
76784
77259
  let needsPseudoParameters = false;
76785
77260
  let needsStateResources = false;
77261
+ let needsEnvOrSecretSubstitution = false;
76786
77262
  for (const res of Object.values(resources)) {
76787
77263
  if (res.Type !== "AWS::ECS::TaskDefinition")
76788
77264
  continue;
@@ -76791,19 +77267,42 @@ function detectEcsImageResolutionNeeds(stack) {
76791
77267
  for (const c of containers) {
76792
77268
  if (!c || typeof c !== "object")
76793
77269
  continue;
76794
- const image = c["Image"];
77270
+ const co = c;
77271
+ const image = co["Image"];
76795
77272
  const need = inspectImageForSubstitutions(image, resources);
76796
77273
  if (need.pseudo)
76797
77274
  needsPseudoParameters = true;
76798
77275
  if (need.state)
76799
77276
  needsStateResources = true;
76800
- if (needsPseudoParameters && needsStateResources)
76801
- break;
77277
+ if (containerHasIntrinsicEnvOrSecret(co))
77278
+ needsEnvOrSecretSubstitution = true;
76802
77279
  }
76803
- if (needsPseudoParameters && needsStateResources)
76804
- break;
76805
77280
  }
76806
- return { needsPseudoParameters, needsStateResources };
77281
+ return { needsPseudoParameters, needsStateResources, needsEnvOrSecretSubstitution };
77282
+ }
77283
+ function containerHasIntrinsicEnvOrSecret(c) {
77284
+ const env = c["Environment"];
77285
+ if (Array.isArray(env)) {
77286
+ for (const entry of env) {
77287
+ if (!entry || typeof entry !== "object")
77288
+ continue;
77289
+ const v = entry["Value"];
77290
+ if (v !== void 0 && typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") {
77291
+ return true;
77292
+ }
77293
+ }
77294
+ }
77295
+ const secrets = c["Secrets"];
77296
+ if (Array.isArray(secrets)) {
77297
+ for (const entry of secrets) {
77298
+ if (!entry || typeof entry !== "object")
77299
+ continue;
77300
+ const v = entry["ValueFrom"];
77301
+ if (v !== void 0 && typeof v !== "string")
77302
+ return true;
77303
+ }
77304
+ }
77305
+ return false;
76807
77306
  }
76808
77307
  function inspectImageForSubstitutions(image, resources) {
76809
77308
  if (!image || typeof image !== "object")
@@ -77004,6 +77503,11 @@ function extractTaskDefinitionProperties(stack, logicalId, resource, context) {
77004
77503
  const containers = rawContainers.map(
77005
77504
  (c, idx) => parseContainerDefinition(c, idx, logicalId, resources, stack, context)
77006
77505
  );
77506
+ for (const ctr of containers) {
77507
+ for (const w of ctr.warnings) {
77508
+ warnings.push(`Container '${ctr.name}': ${w}`);
77509
+ }
77510
+ }
77007
77511
  const rawVolumes = props["Volumes"];
77008
77512
  const volumes = Array.isArray(rawVolumes) ? rawVolumes.map((v, idx) => parseVolume(v, idx, logicalId)) : [];
77009
77513
  const containerNames = new Set(containers.map((c) => c.name));
@@ -77065,7 +77569,9 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77065
77569
  const command = pickStringArray2(c["Command"]);
77066
77570
  const entryPoint = pickStringArray2(c["EntryPoint"]);
77067
77571
  const workingDirectory = pickString(c["WorkingDirectory"]);
77572
+ const subContext = buildSubstitutionContextFromImageContext(context);
77068
77573
  const environment = {};
77574
+ const droppedEnvKeys = [];
77069
77575
  if (Array.isArray(c["Environment"])) {
77070
77576
  for (const entry of c["Environment"]) {
77071
77577
  if (!entry || typeof entry !== "object")
@@ -77077,19 +77583,54 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77077
77583
  continue;
77078
77584
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
77079
77585
  environment[key] = String(value);
77586
+ continue;
77587
+ }
77588
+ if (subContext) {
77589
+ const sub = substituteAgainstState(value, subContext);
77590
+ if (sub.kind === "literal") {
77591
+ environment[key] = String(sub.value);
77592
+ continue;
77593
+ }
77594
+ droppedEnvKeys.push({ key, reason: sub.reason });
77595
+ } else {
77596
+ droppedEnvKeys.push({
77597
+ key,
77598
+ reason: "intrinsic-valued; pass --from-state to substitute against deployed state"
77599
+ });
77080
77600
  }
77081
77601
  }
77082
77602
  }
77083
77603
  const secrets = [];
77604
+ const droppedSecretKeys = [];
77084
77605
  if (Array.isArray(c["Secrets"])) {
77085
77606
  for (const entry of c["Secrets"]) {
77086
77607
  if (!entry || typeof entry !== "object")
77087
77608
  continue;
77088
77609
  const e = entry;
77089
77610
  const sName = pickString(e["Name"]);
77090
- const valueFrom = pickString(e["ValueFrom"]);
77091
- if (sName && valueFrom)
77092
- secrets.push({ name: sName, valueFrom });
77611
+ const valueFromRaw = e["ValueFrom"];
77612
+ if (!sName)
77613
+ continue;
77614
+ if (typeof valueFromRaw === "string" && valueFromRaw.length > 0) {
77615
+ secrets.push({ name: sName, valueFrom: valueFromRaw });
77616
+ continue;
77617
+ }
77618
+ if (subContext) {
77619
+ const sub = substituteAgainstState(valueFromRaw, subContext);
77620
+ if (sub.kind === "literal" && typeof sub.value === "string" && sub.value.length > 0) {
77621
+ secrets.push({ name: sName, valueFrom: sub.value });
77622
+ continue;
77623
+ }
77624
+ droppedSecretKeys.push({
77625
+ key: sName,
77626
+ reason: sub.kind === "literal" ? "resolved to non-string / empty value" : sub.reason
77627
+ });
77628
+ } else {
77629
+ droppedSecretKeys.push({
77630
+ key: sName,
77631
+ reason: "intrinsic-valued ValueFrom; pass --from-state to resolve the deployed ARN"
77632
+ });
77633
+ }
77093
77634
  }
77094
77635
  }
77095
77636
  const portMappings = [];
@@ -77179,6 +77720,13 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77179
77720
  ulimits.push({ name: uName, softLimit: soft, hardLimit: hard });
77180
77721
  }
77181
77722
  }
77723
+ const warnings = [];
77724
+ for (const d of droppedEnvKeys) {
77725
+ warnings.push(`Environment '${d.key}' dropped: ${d.reason}`);
77726
+ }
77727
+ for (const d of droppedSecretKeys) {
77728
+ warnings.push(`Secret '${d.key}' dropped: ${d.reason}`);
77729
+ }
77182
77730
  const out = {
77183
77731
  name,
77184
77732
  image,
@@ -77189,7 +77737,8 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77189
77737
  dependsOn,
77190
77738
  links,
77191
77739
  essential,
77192
- ulimits
77740
+ ulimits,
77741
+ warnings
77193
77742
  };
77194
77743
  if (command !== void 0)
77195
77744
  out.command = command;
@@ -77207,6 +77756,15 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77207
77756
  out.readonlyRootFilesystem = readonlyRootFilesystem;
77208
77757
  return out;
77209
77758
  }
77759
+ function buildSubstitutionContextFromImageContext(context) {
77760
+ if (!context?.stateResources)
77761
+ return void 0;
77762
+ const subContext = { resources: context.stateResources };
77763
+ if (context.pseudoParameters) {
77764
+ subContext.pseudoParameters = { ...context.pseudoParameters };
77765
+ }
77766
+ return subContext;
77767
+ }
77210
77768
  function parseContainerImage(raw, containerName, taskLogicalId, resources, _stack, context) {
77211
77769
  const getAttImage = tryResolveImageGetAtt(raw, resources, context);
77212
77770
  if (getAttImage) {
@@ -77258,73 +77816,37 @@ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stac
77258
77816
  }
77259
77817
  const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(substituted);
77260
77818
  if (ecrMatch) {
77261
- return { kind: "ecr", uri: substituted, account: ecrMatch[1], region: ecrMatch[2] };
77262
- }
77263
- return { kind: "public", uri: substituted };
77264
- }
77265
- function findUnresolvedEcrRepositoryRef(substituted, resources) {
77266
- const placeholderRegex = /\$\{([^}]+)\}/g;
77267
- let m;
77268
- while ((m = placeholderRegex.exec(substituted)) !== null) {
77269
- const key = m[1];
77270
- if (key.startsWith("AWS::"))
77271
- continue;
77272
- const dot = key.indexOf(".");
77273
- const lid = dot === -1 ? key : key.slice(0, dot);
77274
- if (resources[lid]?.Type === "AWS::ECR::Repository")
77275
- return lid;
77276
- }
77277
- return void 0;
77278
- }
77279
- function classifyResolvedImage(uri) {
77280
- if (uri.includes("cdk-hnb659fds-container-assets-")) {
77281
- const hashMatch = /:([a-f0-9]{8,})$/.exec(uri);
77282
- const out = { kind: "cdk-asset" };
77283
- if (hashMatch)
77284
- out.assetHash = hashMatch[1];
77285
- return out;
77286
- }
77287
- const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(uri);
77288
- if (ecrMatch) {
77289
- return { kind: "ecr", uri, account: ecrMatch[1], region: ecrMatch[2] };
77290
- }
77291
- return { kind: "public", uri };
77292
- }
77293
- function substituteImagePlaceholders(flat, resources, context) {
77294
- if (!flat.includes("${"))
77295
- return flat;
77296
- return flat.replace(/\$\{([^}]+)\}/g, (full, key) => {
77297
- if (context?.pseudoParameters) {
77298
- if (key === "AWS::AccountId" && context.pseudoParameters.accountId) {
77299
- return context.pseudoParameters.accountId;
77300
- }
77301
- if (key === "AWS::Region" && context.pseudoParameters.region) {
77302
- return context.pseudoParameters.region;
77303
- }
77304
- if (key === "AWS::Partition" && context.pseudoParameters.partition) {
77305
- return context.pseudoParameters.partition;
77306
- }
77307
- if (key === "AWS::URLSuffix" && context.pseudoParameters.urlSuffix) {
77308
- return context.pseudoParameters.urlSuffix;
77309
- }
77310
- }
77311
- if (context?.stateResources) {
77312
- const dot = key.indexOf(".");
77313
- const logicalId = dot === -1 ? key : key.slice(0, dot);
77314
- const refResource = resources[logicalId];
77315
- const stateEntry = context.stateResources[logicalId];
77316
- if (refResource?.Type === "AWS::ECR::Repository" && stateEntry) {
77317
- if (dot === -1) {
77318
- return stateEntry.physicalId;
77319
- }
77320
- const attr = key.slice(dot + 1);
77321
- const cached = stateEntry.attributes?.[attr];
77322
- if (typeof cached === "string")
77323
- return cached;
77324
- }
77325
- }
77326
- return full;
77327
- });
77819
+ return { kind: "ecr", uri: substituted, account: ecrMatch[1], region: ecrMatch[2] };
77820
+ }
77821
+ return { kind: "public", uri: substituted };
77822
+ }
77823
+ function findUnresolvedEcrRepositoryRef(substituted, resources) {
77824
+ const placeholderRegex = /\$\{([^}]+)\}/g;
77825
+ let m;
77826
+ while ((m = placeholderRegex.exec(substituted)) !== null) {
77827
+ const key = m[1];
77828
+ if (key.startsWith("AWS::"))
77829
+ continue;
77830
+ const dot = key.indexOf(".");
77831
+ const lid = dot === -1 ? key : key.slice(0, dot);
77832
+ if (resources[lid]?.Type === "AWS::ECR::Repository")
77833
+ return lid;
77834
+ }
77835
+ return void 0;
77836
+ }
77837
+ function classifyResolvedImage(uri) {
77838
+ if (uri.includes("cdk-hnb659fds-container-assets-")) {
77839
+ const hashMatch = /:([a-f0-9]{8,})$/.exec(uri);
77840
+ const out = { kind: "cdk-asset" };
77841
+ if (hashMatch)
77842
+ out.assetHash = hashMatch[1];
77843
+ return out;
77844
+ }
77845
+ const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(uri);
77846
+ if (ecrMatch) {
77847
+ return { kind: "ecr", uri, account: ecrMatch[1], region: ecrMatch[2] };
77848
+ }
77849
+ return { kind: "public", uri };
77328
77850
  }
77329
77851
  function tryResolveImageGetAtt(raw, resources, context) {
77330
77852
  if (!raw || typeof raw !== "object")
@@ -77377,228 +77899,6 @@ function extractImageString(value) {
77377
77899
  }
77378
77900
  return void 0;
77379
77901
  }
77380
- function tryResolveImageFnJoin(raw, resources, context) {
77381
- if (!raw || typeof raw !== "object")
77382
- return { kind: "not-applicable" };
77383
- const obj = raw;
77384
- const arg = obj["Fn::Join"];
77385
- if (arg === void 0)
77386
- return { kind: "not-applicable" };
77387
- if (!Array.isArray(arg) || arg.length !== 2 || !Array.isArray(arg[1])) {
77388
- return { kind: "unsupported-join", reason: "Fn::Join must be [delimiter, [elements]]" };
77389
- }
77390
- const [delimiter, elements] = arg;
77391
- if (typeof delimiter !== "string") {
77392
- return {
77393
- kind: "unsupported-join",
77394
- reason: `Fn::Join delimiter must be a string, got ${typeof delimiter}`
77395
- };
77396
- }
77397
- const repoLogicalId = findEcrRepositoryRefInTree(elements, resources);
77398
- const stateResources = context?.stateResources;
77399
- if (repoLogicalId && !stateResources) {
77400
- return { kind: "needs-state", repoLogicalId };
77401
- }
77402
- const parts = [];
77403
- for (const element of elements) {
77404
- const r = resolveImageIntrinsic(element, resources, context);
77405
- if (r === void 0) {
77406
- if (!repoLogicalId)
77407
- return { kind: "not-applicable" };
77408
- return {
77409
- kind: "unsupported-join",
77410
- reason: "one or more Fn::Join elements could not be resolved"
77411
- };
77412
- }
77413
- parts.push(r);
77414
- }
77415
- return { kind: "resolved", uri: parts.join(delimiter) };
77416
- }
77417
- function findEcrRepositoryRefInTree(node, resources) {
77418
- if (node === null || node === void 0)
77419
- return void 0;
77420
- if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
77421
- return void 0;
77422
- }
77423
- if (Array.isArray(node)) {
77424
- for (const item of node) {
77425
- const hit = findEcrRepositoryRefInTree(item, resources);
77426
- if (hit)
77427
- return hit;
77428
- }
77429
- return void 0;
77430
- }
77431
- if (typeof node !== "object")
77432
- return void 0;
77433
- const obj = node;
77434
- if (typeof obj["Ref"] === "string") {
77435
- const target = obj["Ref"];
77436
- if (resources[target]?.Type === "AWS::ECR::Repository")
77437
- return target;
77438
- return void 0;
77439
- }
77440
- const getAtt = obj["Fn::GetAtt"];
77441
- if (getAtt !== void 0) {
77442
- let lid;
77443
- if (Array.isArray(getAtt) && typeof getAtt[0] === "string")
77444
- lid = getAtt[0];
77445
- else if (typeof getAtt === "string")
77446
- lid = getAtt.split(".")[0];
77447
- if (lid && resources[lid]?.Type === "AWS::ECR::Repository")
77448
- return lid;
77449
- return void 0;
77450
- }
77451
- for (const value of Object.values(obj)) {
77452
- const hit = findEcrRepositoryRefInTree(value, resources);
77453
- if (hit)
77454
- return hit;
77455
- }
77456
- return void 0;
77457
- }
77458
- function resolveImageIntrinsic(node, resources, context) {
77459
- const v = resolveImageIntrinsicAny(node, resources, context);
77460
- if (typeof v === "string")
77461
- return v;
77462
- if (typeof v === "number" || typeof v === "boolean")
77463
- return String(v);
77464
- return void 0;
77465
- }
77466
- function resolveImageIntrinsicAny(node, resources, context) {
77467
- if (node === null || node === void 0)
77468
- return void 0;
77469
- if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
77470
- return node;
77471
- }
77472
- if (Array.isArray(node)) {
77473
- return void 0;
77474
- }
77475
- if (typeof node !== "object")
77476
- return void 0;
77477
- const obj = node;
77478
- const keys = Object.keys(obj);
77479
- if (keys.length !== 1)
77480
- return void 0;
77481
- const intrinsic = keys[0];
77482
- const arg = obj[intrinsic];
77483
- if (intrinsic === "Ref") {
77484
- if (typeof arg !== "string")
77485
- return void 0;
77486
- if (arg.startsWith("AWS::")) {
77487
- const p = context?.pseudoParameters;
77488
- if (!p)
77489
- return void 0;
77490
- if (arg === "AWS::URLSuffix")
77491
- return p.urlSuffix;
77492
- if (arg === "AWS::Partition")
77493
- return p.partition;
77494
- if (arg === "AWS::Region")
77495
- return p.region;
77496
- if (arg === "AWS::AccountId")
77497
- return p.accountId;
77498
- return void 0;
77499
- }
77500
- const refResource = resources[arg];
77501
- if (refResource?.Type !== "AWS::ECR::Repository")
77502
- return void 0;
77503
- const stateEntry = context?.stateResources?.[arg];
77504
- if (!stateEntry)
77505
- return void 0;
77506
- return stateEntry.physicalId;
77507
- }
77508
- if (intrinsic === "Fn::GetAtt") {
77509
- let logicalId;
77510
- let attr;
77511
- if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
77512
- logicalId = arg[0];
77513
- attr = arg[1];
77514
- } else if (typeof arg === "string") {
77515
- const dot = arg.indexOf(".");
77516
- if (dot > 0 && dot < arg.length - 1) {
77517
- logicalId = arg.slice(0, dot);
77518
- attr = arg.slice(dot + 1);
77519
- }
77520
- }
77521
- if (!logicalId || !attr)
77522
- return void 0;
77523
- if (resources[logicalId]?.Type !== "AWS::ECR::Repository")
77524
- return void 0;
77525
- const cached = context?.stateResources?.[logicalId]?.attributes?.[attr];
77526
- if (typeof cached === "string" && cached.length > 0)
77527
- return cached;
77528
- return void 0;
77529
- }
77530
- if (intrinsic === "Fn::Split") {
77531
- if (!Array.isArray(arg) || arg.length !== 2)
77532
- return void 0;
77533
- const argArr = arg;
77534
- const delim = argArr[0];
77535
- if (typeof delim !== "string")
77536
- return void 0;
77537
- const src = resolveImageIntrinsicAny(argArr[1], resources, context);
77538
- if (typeof src !== "string")
77539
- return void 0;
77540
- return src.split(delim);
77541
- }
77542
- if (intrinsic === "Fn::Select") {
77543
- if (!Array.isArray(arg) || arg.length !== 2)
77544
- return void 0;
77545
- const argArr = arg;
77546
- const rawIndex = argArr[0];
77547
- let index;
77548
- if (typeof rawIndex === "number") {
77549
- index = rawIndex;
77550
- } else if (typeof rawIndex === "string" && /^-?\d+$/.test(rawIndex)) {
77551
- index = Number.parseInt(rawIndex, 10);
77552
- }
77553
- if (index === void 0 || !Number.isFinite(index))
77554
- return void 0;
77555
- const list = resolveImageIntrinsicAny(argArr[1], resources, context);
77556
- if (Array.isArray(list)) {
77557
- if (index < 0 || index >= list.length)
77558
- return void 0;
77559
- const picked = list[index];
77560
- if (typeof picked === "string")
77561
- return picked;
77562
- return void 0;
77563
- }
77564
- if (Array.isArray(argArr[1])) {
77565
- const listLiteral = argArr[1];
77566
- if (index < 0 || index >= listLiteral.length)
77567
- return void 0;
77568
- return resolveImageIntrinsic(listLiteral[index], resources, context);
77569
- }
77570
- return void 0;
77571
- }
77572
- if (intrinsic === "Fn::Join") {
77573
- if (!Array.isArray(arg) || arg.length !== 2)
77574
- return void 0;
77575
- const [delim, parts] = arg;
77576
- if (typeof delim !== "string" || !Array.isArray(parts))
77577
- return void 0;
77578
- const resolved = [];
77579
- for (const part of parts) {
77580
- const r = resolveImageIntrinsic(part, resources, context);
77581
- if (r === void 0)
77582
- return void 0;
77583
- resolved.push(r);
77584
- }
77585
- return resolved.join(delim);
77586
- }
77587
- if (intrinsic === "Fn::Sub") {
77588
- let template;
77589
- if (typeof arg === "string")
77590
- template = arg;
77591
- else if (Array.isArray(arg) && typeof arg[0] === "string")
77592
- template = arg[0];
77593
- if (template === void 0)
77594
- return void 0;
77595
- const out = substituteImagePlaceholders(template, resources, context);
77596
- if (out.includes("${"))
77597
- return void 0;
77598
- return out;
77599
- }
77600
- return void 0;
77601
- }
77602
77902
  function parseVolume(raw, idx, taskLogicalId) {
77603
77903
  if (!raw || typeof raw !== "object") {
77604
77904
  throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] is not an object.`);
@@ -78659,14 +78959,16 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78659
78959
  if (!candidate)
78660
78960
  return void 0;
78661
78961
  const needs = detectEcsImageResolutionNeeds(candidate);
78662
- if (!needs.needsPseudoParameters && !needs.needsStateResources)
78962
+ if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) {
78663
78963
  return void 0;
78964
+ }
78664
78965
  const ctx = {};
78665
- if (needs.needsPseudoParameters) {
78966
+ const wantsPseudoForEnvOrSecret = options.fromState && needs.needsEnvOrSecretSubstitution;
78967
+ if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
78666
78968
  const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
78667
78969
  if (!region) {
78668
78970
  logger.warn(
78669
- "Container Image references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack."
78971
+ "Resolver references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack."
78670
78972
  );
78671
78973
  }
78672
78974
  let accountId;
@@ -78674,7 +78976,7 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78674
78976
  accountId = await resolveCallerAccountId(region);
78675
78977
  } catch (err) {
78676
78978
  logger.warn(
78677
- `Container Image references \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; the resolver will surface its existing error.`
78979
+ `Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`
78678
78980
  );
78679
78981
  }
78680
78982
  const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
@@ -78687,7 +78989,8 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78687
78989
  }
78688
78990
  };
78689
78991
  }
78690
- if (options.fromState && needs.needsStateResources) {
78992
+ const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
78993
+ if (options.fromState && wantsState) {
78691
78994
  const loaded = await loadStateForStack(candidate.stackName, candidate.region, {
78692
78995
  ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
78693
78996
  ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
@@ -78702,6 +79005,10 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78702
79005
  logger.warn(
78703
79006
  "Container Image references a same-stack AWS::ECR::Repository. Pass --from-state to substitute the deployed repository URI (requires the stack to have been deployed via cdkd deploy). Otherwise the resolver will surface its existing error."
78704
79007
  );
79008
+ } else if (!options.fromState && needs.needsEnvOrSecretSubstitution) {
79009
+ logger.warn(
79010
+ "Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass --from-state to substitute them against the deployed cdkd state. Without --from-state these entries are dropped (per-key warnings will follow)."
79011
+ );
78705
79012
  }
78706
79013
  return ctx;
78707
79014
  }
@@ -80286,7 +80593,7 @@ function reorderArgs(argv) {
80286
80593
  }
80287
80594
  async function main() {
80288
80595
  const program = new Command18();
80289
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.91.1");
80596
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.91.3");
80290
80597
  program.addCommand(createBootstrapCommand());
80291
80598
  program.addCommand(createSynthCommand());
80292
80599
  program.addCommand(createListCommand());