@go-to-k/cdkd 0.94.14 → 0.95.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/README.md CHANGED
@@ -160,12 +160,20 @@ cdkd has three command families:
160
160
  use them to inspect / clean up state when the source is gone or
161
161
  you don't want to synth. `cdkd state destroy` is the CDK-app-free
162
162
  counterpart of `cdkd destroy`.
163
- - **`cdkd local ...` subcommands** (`local invoke`, `local start-api`)
164
- run synthesized Lambda functions locally inside Docker containers that
165
- bundle the AWS Lambda Runtime Interface Emulator (RIE). `local invoke`
166
- runs a single Lambda once; `local start-api` stands up a long-running
167
- HTTP server that maps API Gateway / HTTP API / Function URL routes to
168
- local Lambda invocations. No AWS API calls, no state bucket needed.
163
+ - **`cdkd local ...` subcommands** (`local invoke`, `local start-api`,
164
+ `local run-task`) run synthesized workloads locally inside Docker
165
+ containers. The Lambda variants (`local invoke` / `local start-api`)
166
+ bundle the AWS Lambda Runtime Interface Emulator (RIE); `local invoke`
167
+ runs a single Lambda once, and `local start-api` stands up a
168
+ long-running HTTP server that maps API Gateway / HTTP API / Function
169
+ URL routes to local Lambda invocations. `local run-task` is the ECS
170
+ counterpart — it locates an `AWS::ECS::TaskDefinition` from the
171
+ synthesized template and stands up every container in `dependsOn`
172
+ order on a per-task docker network with the AWS-published metadata
173
+ endpoints sidecar, so containers see `ECS_CONTAINER_METADATA_URI_V4`
174
+ (and optionally task-role creds via `--assume-task-role`) just like
175
+ they would on Fargate / ECS. No AWS API calls beyond optional STS /
176
+ Secrets resolution, no state bucket needed.
169
177
 
170
178
  Options like `--app`, `--state-bucket`, and `--context` can be omitted if configured via `cdk.json` or environment variables (`CDKD_APP`, `CDKD_STATE_BUCKET`).
171
179
 
@@ -376,6 +384,71 @@ Lambda-ServiceToken Active wait).
376
384
  See [docs/cli-reference.md](docs/cli-reference.md) for the full
377
385
  type-pair allowlist and trade-off notes.
378
386
 
387
+ ## Local execution
388
+
389
+ The `cdkd local` family runs AWS workloads on the developer's machine
390
+ via Docker — Lambda functions, API Gateway routes, and ECS tasks —
391
+ without an AWS deploy. Modeled on `sam local *` but reuses cdkd's
392
+ synthesis / asset / construct-path plumbing — no `template.yaml` to
393
+ maintain, no `cdk synth | sam ...` round-trip.
394
+
395
+ | Subcommand | Emulates |
396
+ | --- | --- |
397
+ | `cdkd local invoke <target>` | One-shot Lambda invoke via the AWS Lambda Runtime Interface Emulator (RIE) |
398
+ | `cdkd local start-api` | Long-running HTTP server for REST v1 / HTTP API / Function URL routes |
399
+ | `cdkd local run-task <target>` | ECS RunTask — every container in a task definition started on a per-task docker network |
400
+
401
+ Requires Docker. Pass `--from-state` to substitute deployed physical
402
+ IDs into intrinsic-valued properties; without it, intrinsic values are
403
+ dropped with a per-key warning (matches `sam local *` semantics).
404
+
405
+ ### `local invoke`
406
+
407
+ ```bash
408
+ cdkd local invoke MyStack/MyApi/Handler # one-shot invoke
409
+ cdkd local invoke MyStack/Handler --event events/get.json
410
+ cdkd local invoke MyStack/Handler --from-state # recover deployed env vars
411
+ ```
412
+
413
+ Supports every current AWS Lambda runtime (Node.js / Python / Ruby /
414
+ Java / .NET / `provided.al2023`), container Lambdas
415
+ (`DockerImageFunction` / `Code.ImageUri`) via local-build or ECR pull,
416
+ and same-stack Lambda Layers bind-mounted at `/opt`.
417
+
418
+ ### `local start-api`
419
+
420
+ ```bash
421
+ cdkd local start-api # one HTTP server per discovered API
422
+ cdkd local start-api --port 3000 # pin the first server's port
423
+ cdkd local start-api --api MyHttpApi # filter by logical id
424
+ cdkd local start-api --api MyStack/MyHttpApi # OR: CDK Construct path
425
+ cdkd local start-api --warm --watch # pre-start + hot reload
426
+ ```
427
+
428
+ One server per discovered API — authorizers, CORS configs, and stage
429
+ variables stay scoped to the owning API. Supports REST v1 + HTTP API +
430
+ Function URL with AWS_PROXY integrations; Lambda TOKEN / REQUEST,
431
+ Cognito User Pool, and HTTP v2 JWT authorizers (JWKS-verified); CORS
432
+ preflight; hot reload via `--watch`.
433
+
434
+ ### `local run-task`
435
+
436
+ ```bash
437
+ cdkd local run-task MyStack/MyService/TaskDef
438
+ cdkd local run-task MyTaskDef --from-state # resolve deployed secrets / env intrinsics
439
+ ```
440
+
441
+ Starts every container in the task definition on a per-task docker
442
+ network with the AWS-published ECS metadata sidecar
443
+ (`amazon/amazon-ecs-local-container-endpoints`). `DependsOn` /
444
+ `Secrets` / `Volumes` (Host / Docker) are honored; `Secrets[].ValueFrom`
445
+ is resolved from SecretsManager / SSM at startup.
446
+
447
+ See [docs/local-emulation.md](docs/local-emulation.md) for the full
448
+ reference — supported runtimes, target resolution, every flag, exit
449
+ codes, route precedence, container-pool semantics, networking model,
450
+ v1 scope notes.
451
+
379
452
  ## Importing existing resources
380
453
 
381
454
  `cdkd import` adopts AWS resources that are already deployed (via
@@ -456,85 +529,6 @@ Two `orphan` variants at different granularities:
456
529
  Both `cdkd destroy` (synth-driven) and `cdkd state destroy`
457
530
  (state-driven, no synth) delete AWS resources + state.
458
531
 
459
- ## Stack-name prefix on physical names
460
-
461
- By default cdkd creates AWS resources with the **exact name you
462
- declared** in CDK code: `new iam.Role(this, 'CRRole', { roleName:
463
- 'my-role' })` in stack `MyStack` produces an AWS resource named
464
- `my-role`. Consistent across every resource type. This is the
465
- default since **v0.94.0** (closes [#299](https://github.com/go-to-k/cdkd/issues/299)).
466
-
467
- Pre-v0.94.0 cdkd prepended the stack name to user-declared physical
468
- names on a subset of types only (Pattern B providers: IAM Role /
469
- User / Group / InstanceProfile / ELBv2 LoadBalancer / TargetGroup),
470
- while Pattern A providers (Lambda, S3, SNS, SQS, DynamoDB, etc.) used
471
- the user's name as-is. The inconsistency was opaque to users and
472
- surfaced as failures in `cdkd export` (CFn IMPORT identifier
473
- mismatch). Flipping the default brings every resource type into line
474
- out of the box.
475
-
476
- `cdkd deploy --prefix-user-supplied-names` opts BACK in to the
477
- legacy prefixing on Pattern B providers (matching pre-v0.94.0 cdkd).
478
- Useful when migrating an existing stack that was originally deployed
479
- under the legacy default and you don't want to take a one-time
480
- replacement on every Pattern B resource.
481
-
482
- | | Default (no flag) | `--prefix-user-supplied-names` |
483
- | --- | --- | --- |
484
- | `new iam.Role({ roleName: 'my-role' })` | `my-role` | `MyStack-my-role` (legacy) |
485
- | `new s3.Bucket({ bucketName: 'my-bucket' })` | `my-bucket` (always — Pattern A) | `my-bucket` (unchanged) |
486
- | `new iam.Role(...)` (no `roleName`) | `MyStack-CRRole-<hash>` (auto-generated; prefix kept for uniqueness) | `MyStack-CRRole-<hash>` (unchanged) |
487
-
488
- Resolution chain (highest wins): `--prefix-user-supplied-names`
489
- CLI flag → `CDKD_PREFIX_USER_SUPPLIED_NAMES=true` env var →
490
- `cdk.json` `context.cdkd.prefixUserSuppliedNames: true` → default
491
- `false` (skip prefix).
492
-
493
- The deprecated `--no-prefix-user-supplied-names` flag (plus the
494
- `CDKD_NO_PREFIX_USER_SUPPLIED_NAMES` env var and `cdk.json
495
- context.cdkd.noPrefixUserSuppliedNames`) is still accepted but now
496
- matches the default; setting it emits a deprecation warning and is a
497
- no-op. Remove it from your CLI invocations and config.
498
-
499
- ### Migration from pre-v0.94.0
500
-
501
- This is a **breaking change**: upgrading from a pre-v0.94.0 cdkd to
502
- v0.94.0+ flips the AWS-resource name cdkd produces on Pattern B
503
- providers (IAM Role / User / Group / InstanceProfile / ELBv2 LB / TG)
504
- with user-supplied physical names. The next `cdkd deploy` against an
505
- existing stack will propose REPLACEMENT on every such resource —
506
- the AWS resource has the prefixed name; the new template intent has
507
- the un-prefixed name.
508
-
509
- Pick one of:
510
-
511
- 1. **Accept the one-time replacement** (simplest; only safe when the
512
- types involved tolerate replacement — IAM Roles get fresh ARNs,
513
- ELBv2 LBs get fresh DNS names).
514
- 2. **Pin legacy prefixing**: pass `--prefix-user-supplied-names`,
515
- set `CDKD_PREFIX_USER_SUPPLIED_NAMES=true`, or add
516
- `"prefixUserSuppliedNames": true` under `cdk.json` `context.cdkd`.
517
- 3. **Drop the explicit physical name** in CDK code where you don't
518
- actually need a stable name — `new iam.Role(...)` without
519
- `roleName` always uses the auto-generated `MyStack-CRRole-<hash>`
520
- form regardless of this flag.
521
-
522
- A migration helper (`cdkd state rename-strip-prefix <stack>`) that
523
- would let an existing stack adopt the new default without replacement
524
- is tracked separately in [#300](https://github.com/go-to-k/cdkd/issues/300).
525
-
526
- ### Effect on `cdkd export`
527
-
528
- [PR #285 `cdkd export`](https://github.com/go-to-k/cdkd/pull/285)
529
- surfaced the pre-v0.94.0 inconsistency: the CFn IMPORT changeset's
530
- identifier check would fail on a synth `RoleName: 'my-role'` vs the
531
- AWS-deployed `MyStack-my-role`, so the export command overlays
532
- `ResourceIdentifier` onto `Properties` to bridge the gap. The
533
- overlay is still needed for stacks deployed under the legacy default
534
- (or with `--prefix-user-supplied-names`); a fresh stack deployed
535
- under the v0.94.0 default has matching names and the overlay is a
536
- no-op for it.
537
-
538
532
  ## `--remove-protection`: one-shot bypass for protected resources
539
533
 
540
534
  CDK's `new Stack(app, 'X', { terminationProtection: true })` is honored
@@ -594,182 +588,6 @@ cdkd publish-assets -a cdk.out # skip synth, use pre-synthesized assembly
594
588
  See [docs/cli-reference.md](docs/cli-reference.md#publish-assets-synth--build--publish-no-deploy)
595
589
  for stack-selection rules and concurrency knobs.
596
590
 
597
- ## `local invoke`: run Lambda functions locally
598
-
599
- `cdkd local invoke <target>` runs a Lambda function from a CDK app on the
600
- developer's machine, inside a Docker container that bundles the AWS
601
- Lambda Runtime Interface Emulator (RIE). Modeled on `sam local invoke`
602
- but reusing cdkd's synthesis / asset / construct-path plumbing — no
603
- `template.yaml` to maintain, no `cdk synth | sam ...` round-trip.
604
-
605
- Requires Docker. Supports every current AWS Lambda runtime
606
- (`nodejs18.x` / `nodejs20.x` / `nodejs22.x` / `nodejs24.x` / `python3.11` /
607
- `python3.12` / `python3.13` / `python3.14` / `ruby3.2` / `ruby3.3` /
608
- `java8.al2` / `java11` / `java17` / `java21` / `dotnet6` / `dotnet8` /
609
- `provided.al2` / `provided.al2023`). The deprecated `go1.x` runtime is
610
- rejected with a migration pointer to `provided.al2023`. Java, .NET, and
611
- `provided.*` Lambdas are **asset-backed only** — the Handler shape names
612
- a compiled artifact (`package.Class::method` for Java's JVM class;
613
- `Assembly::Namespace.Class::Method` for .NET's CLR assembly; an
614
- arbitrary `bootstrap` binary for the OS-only `provided.*` runtimes), so
615
- use `lambda.Code.fromAsset(<dir>)` with a directory containing the
616
- compiled output (`.class` hierarchy / `.jar` / `.dll` / native binary);
617
- inline `Code.ZipFile` is rejected with a clear routing message.
618
-
619
- **Container Lambdas** — `lambda.DockerImageFunction(...)` /
620
- `Code.ImageUri` is supported alongside ZIP Lambdas. cdkd reads the
621
- function's local `Dockerfile` from `cdk.out` and runs `docker build`
622
- locally before invoking. When no asset matches (typically: invoking a
623
- stack deployed elsewhere), cdkd falls back to `docker pull` from
624
- ECR — same-account / same-region only in v1; cross-account /
625
- cross-region is not yet supported. `Architectures: [x86_64]` /
626
- `[arm64]` are honored via `--platform` so an arm64 host running an
627
- x86_64 Lambda doesn't hit emulation.
628
-
629
- ```bash
630
- # Invoke by CDK display path (single-stack apps may omit the prefix)
631
- cdkd local invoke MyStack/MyApi/Handler
632
- cdkd local invoke MyStack:MyApiHandler1234ABCD # logical-id form
633
-
634
- # Pass an event payload
635
- cdkd local invoke MyStack/Handler --event events/get.json
636
- echo '{"path":"/"}' | cdkd local invoke MyStack/Handler --event-stdin
637
-
638
- # Override env vars (SAM-compatible shape: {"LogicalId":{"KEY":"VALUE"}}
639
- # plus an optional top-level "Parameters" block applied to every invoke)
640
- cdkd local invoke MyStack/Handler --env-vars env.json
641
-
642
- # Skip docker pull when iterating
643
- cdkd local invoke MyStack/Handler --no-pull
644
-
645
- # Skip the local docker build for container Lambdas (Code.ImageUri).
646
- # Reuses the deterministic cdkd-local-invoke-<hash> tag from a prior
647
- # build. Errors clearly when the tag is missing.
648
- cdkd local invoke MyStack/ContainerHandler --no-build
649
-
650
- # Run with the deployed function's narrow execution role (otherwise the
651
- # developer's shell credentials are forwarded — SAM-compatible default)
652
- cdkd local invoke MyStack/Handler --assume-role arn:aws:iam::123456789012:role/MyApi-handler-role
653
-
654
- # Attach a Node debugger
655
- cdkd local invoke MyStack/Handler --debug-port 9229
656
-
657
- # After `cdkd deploy`, recover intrinsic-valued env vars (Ref / Fn::GetAtt
658
- # / Fn::Sub) from cdkd's S3 state instead of dropping them. Off by default
659
- # — keeps the local-only / unscoped flow safe; opt in when you want the
660
- # handler to see the deployed physical IDs (S3 bucket names, DDB table
661
- # names, IAM role ARNs, ...). Disambiguate with `--stack-region <region>`
662
- # when the same stack name has state in multiple regions.
663
- cdkd local invoke MyStack/Handler --from-state
664
- ```
665
-
666
- **Lambda Layers** — same-stack
667
- `AWS::Lambda::LayerVersion` references in `Properties.Layers` are
668
- resolved automatically and bind-mounted at `/opt` (read-only) inside
669
- the container. Each layer's unzipped asset directory under `cdk.out/`
670
- becomes one `-v <layerAssetPath>:/opt:ro` mount; multiple layers
671
- stack via Docker overlay layering, and AWS's "last layer wins on
672
- file collision" rule is preserved by keeping the template's input
673
- order. Cross-stack / cross-account / cross-region layer ARNs (literal
674
- ARN strings in `Properties.Layers`) are out of scope for v1 — cdkd
675
- hard-errors with a clear pointer at the offending entry. Container
676
- Lambdas (`Code.ImageUri`) silently ignore `Layers` (matches AWS:
677
- container images bake layers at build time).
678
-
679
- See [docs/cli-reference.md](docs/cli-reference.md#local-invoke-run-lambda-functions-locally)
680
- for the full surface, target-resolution rules, and v1 scope notes.
681
-
682
- ## `local start-api`: long-running local API server
683
-
684
- `cdkd local start-api` stands up a long-running local HTTP server that
685
- maps the synthesized API Gateway routes (REST v1, HTTP API, Function
686
- URL) to local Lambda invocations against the same RIE-backed Docker
687
- containers `cdkd local invoke` uses. Modeled on `sam local start-api`
688
- but reusing cdkd's synthesis / route-discovery plumbing.
689
-
690
- ```bash
691
- # Auto-allocate one port PER discovered API (printed at startup)
692
- cdkd local start-api
693
-
694
- # Pin the FIRST server to port 3000; subsequent APIs get 3001, 3002, ...
695
- cdkd local start-api --port 3000
696
-
697
- # Restrict to a single API by its CDK logical id (HTTP API / REST API logical
698
- # id, or the backing Lambda's logical id for Function URLs)
699
- cdkd local start-api --api MyAdminApi
700
-
701
- # Pre-warm one container per Lambda at server boot — eliminates first-request cold start
702
- cdkd local start-api --warm
703
-
704
- # Override env vars per-Lambda (SAM-shape file)
705
- cdkd local start-api --env-vars env.json
706
-
707
- # Pin the deployed execution role per Lambda (or globally with a bare ARN)
708
- cdkd local start-api --assume-role MyApiHandler=arn:aws:iam::123:role/handler-role
709
-
710
- # Hot reload — re-synth + re-discover routes when cdk.out/ or asset dirs change
711
- cdkd local start-api --watch
712
-
713
- # Select a specific API Gateway Stage (default: the first attached)
714
- cdkd local start-api --stage prod
715
- ```
716
-
717
- **One server per API** (since v0.81): every discovered API surface gets its
718
- own HTTP server on its own port, so authorizers, CORS configs, and stage
719
- variables stay scoped to the owning API and never bleed across APIs that
720
- happen to share a path. `cdkd local start-api` prints one
721
- `Server listening on http://<host>:<port> (<API> (<kind>))` line per
722
- server at startup; pass `--api <id>` to launch only one of them.
723
-
724
- Scope: REST v1 + HTTP API + Function URL with AWS_PROXY integrations.
725
- Authorizers (Lambda TOKEN/REQUEST + Cognito User Pool + HTTP v2 JWT),
726
- VPC-config Lambda warnings, CORS preflight, hot reload, and stage
727
- variables are supported. WebSocket APIs are not.
728
-
729
- **Authorizers**: `Authorization: Bearer <token>`-protected
730
- routes are gated on the authorizer Lambda's response (TOKEN / REQUEST
731
- authorizers, IAM-policy or HTTP v2 simple shape) or on a JWKS-based JWT
732
- verification (Cognito User Pool authorizers, HTTP v2 JWT authorizers).
733
- When the JWKS endpoint is unreachable from the dev machine, cdkd falls
734
- back to **pass-through mode** (every JWT accepted, with a warn line at
735
- startup) — local-dev-only fallback so a corporate proxy doesn't block
736
- iteration. **Do NOT rely on this in any shared environment.**
737
-
738
- **VPC-config Lambdas**: handlers with `Properties.VpcConfig`
739
- still run locally, but the local container is NOT attached to the
740
- deployed VPC's subnets — calls to private RDS / ElastiCache will fail.
741
- cdkd warns at startup naming each affected Lambda; AWS SDK calls still
742
- reach public AWS endpoints via the dev's network as usual.
743
-
744
- **Hot reload (`--watch`)**: re-runs the synth → discover → spec-build
745
- pipeline whenever `cdk.out/` or any of the routed Lambdas' asset
746
- directories change. Routes added / removed / changed swap in
747
- atomically without restarting the HTTP server; in-flight requests
748
- complete against the old container pool while the new pool warms.
749
- Synth failures are non-fatal — the previous version keeps serving and
750
- a warn line names the failure. Off by default; pass `--watch` to
751
- enable.
752
-
753
- **CORS preflight**: HTTP API v2 OPTIONS preflight requests are
754
- intercepted when the API has a `CorsConfiguration` block. The server
755
- matches the request's `Origin` / `Access-Control-Request-Method` /
756
- `Access-Control-Request-Headers` against the configured allowlist and
757
- returns a `204 No Content` with the canonical `Access-Control-Allow-*`
758
- headers. Preflight handling is skipped when the user has registered
759
- an explicit OPTIONS method (their Lambda owns it). REST v1 CORS (Mock
760
- OPTIONS method) is not auto-handled and stays out of scope; use the
761
- deployed API for that case.
762
-
763
- **Stage variables**: `event.stageVariables` is populated from the
764
- selected Stage's `Variables` (REST v1) / `StageVariables` (HTTP API
765
- v2) map. Default selection is the first Stage attached to each API;
766
- pass `--stage <name>` to pick a Stage by `StageName`. Function URL
767
- routes don't have a Stage — `event.stageVariables` stays `null`.
768
-
769
- See [docs/cli-reference.md](docs/cli-reference.md#local-start-api-long-running-local-api-server)
770
- for the full route-discovery rules, container-pool semantics, exit
771
- codes, and per-authorizer-kind detection / response-shape details.
772
-
773
591
  ## State Management
774
592
 
775
593
  State is stored in S3 with optimistic locking via S3 Conditional Writes
package/dist/cli.js CHANGED
@@ -35003,7 +35003,7 @@ function discoverRoutes(stacks) {
35003
35003
  routes.push(...discoverHttpApiRoute(logicalId, resource, template, stack.stackName));
35004
35004
  break;
35005
35005
  case "AWS::Lambda::Url":
35006
- routes.push(...discoverFunctionUrl(logicalId, resource, stack.stackName));
35006
+ routes.push(...discoverFunctionUrl(logicalId, resource, template, stack.stackName));
35007
35007
  break;
35008
35008
  default: break;
35009
35009
  }
@@ -35040,14 +35040,19 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
35040
35040
  if (!restApiLogicalId) throw new Error(`${stackName}/${logicalId} (AWS::ApiGateway::Method): RestApiId must be a { Ref: '...' } reference (got ${shortJson$1(restApiId)}).`);
35041
35041
  const resourceId = props["ResourceId"];
35042
35042
  const path = buildRestV1Path(resourceId, restApiLogicalId, template, stackName, logicalId);
35043
+ const httpMethod = stringifyValue(props["HttpMethod"] ?? "ANY");
35044
+ const stage = pickRestV1Stage(restApiLogicalId, template);
35045
+ const restApiCdkPath = readApiCdkPath(restApiLogicalId, template);
35043
35046
  return [{
35044
- method: stringifyValue(props["HttpMethod"] ?? "ANY"),
35047
+ method: httpMethod,
35045
35048
  pathPattern: path,
35046
35049
  lambdaLogicalId,
35047
35050
  source: "rest-v1",
35048
35051
  apiVersion: "v1",
35049
- stage: pickRestV1Stage(restApiLogicalId, template),
35052
+ stage,
35050
35053
  apiLogicalId: restApiLogicalId,
35054
+ apiStackName: stackName,
35055
+ ...restApiCdkPath !== void 0 && { apiCdkPath: restApiCdkPath },
35051
35056
  declaredAt: `${stackName}/${logicalId}`
35052
35057
  }];
35053
35058
  }
@@ -35139,6 +35144,7 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
35139
35144
  if (integrationProps["IntegrationSubtype"] !== void 0) throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): IntegrationSubtype '${stringifyValue(integrationProps["IntegrationSubtype"])}' is not supported (ApiGatewayV2 service integrations like SQS/EventBridge cannot run locally).`);
35140
35145
  const lambdaLogicalId = resolveLambdaArnIntrinsic(integrationProps["IntegrationUri"], `${stackName}/${integrationLogicalId}.IntegrationUri`);
35141
35146
  const { method, pathPattern } = parseRouteKey(routeKey);
35147
+ const apiCdkPath = readApiCdkPath(apiLogicalId, template);
35142
35148
  return [{
35143
35149
  method,
35144
35150
  pathPattern,
@@ -35147,6 +35153,8 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
35147
35153
  apiVersion: "v2",
35148
35154
  stage: "$default",
35149
35155
  apiLogicalId,
35156
+ apiStackName: stackName,
35157
+ ...apiCdkPath !== void 0 && { apiCdkPath },
35150
35158
  declaredAt: `${stackName}/${logicalId}`
35151
35159
  }];
35152
35160
  }
@@ -35159,23 +35167,38 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
35159
35167
  * we cannot do locally, and RESPONSE_STREAM uses a streaming response shape
35160
35168
  * (`InvokeWithResponseStream`) the RIE container does not implement.
35161
35169
  */
35162
- function discoverFunctionUrl(logicalId, resource, stackName) {
35170
+ function discoverFunctionUrl(logicalId, resource, template, stackName) {
35163
35171
  const props = resource.Properties ?? {};
35164
35172
  const authType = props["AuthType"];
35165
35173
  if (authType !== "NONE") throw new Error(`${stackName}/${logicalId} (AWS::Lambda::Url): AuthType '${String(authType)}' is not supported (only NONE — IAM auth requires SigV4 verification cdkd cannot emulate locally; deferred follow-up PR).`);
35166
35174
  if (props["InvokeMode"] === "RESPONSE_STREAM") throw new Error(`${stackName}/${logicalId} (AWS::Lambda::Url): InvokeMode RESPONSE_STREAM is not supported (deferred follow-up PR).`);
35167
35175
  const targetArn = props["TargetFunctionArn"];
35176
+ const lambdaLogicalId = resolveLambdaArnIntrinsic(targetArn, `${stackName}/${logicalId}.TargetFunctionArn`);
35177
+ const lambdaCdkPath = readApiCdkPath(lambdaLogicalId, template);
35168
35178
  return [{
35169
35179
  method: "ANY",
35170
35180
  pathPattern: "/{proxy+}",
35171
- lambdaLogicalId: resolveLambdaArnIntrinsic(targetArn, `${stackName}/${logicalId}.TargetFunctionArn`),
35181
+ lambdaLogicalId,
35172
35182
  source: "function-url",
35173
35183
  apiVersion: "v2",
35174
35184
  stage: "$default",
35185
+ apiStackName: stackName,
35186
+ ...lambdaCdkPath !== void 0 && { apiCdkPath: lambdaCdkPath },
35175
35187
  declaredAt: `${stackName}/${logicalId}`
35176
35188
  }];
35177
35189
  }
35178
35190
  /**
35191
+ * Read the `aws:cdk:path` metadata of the resource at `logicalId`,
35192
+ * returning the empty string when the resource is missing or the
35193
+ * metadata isn't set. Hides the "may be missing for a hand-rolled
35194
+ * `cfn.Resource`" branch from every call site.
35195
+ */
35196
+ function readApiCdkPath(logicalId, template) {
35197
+ const resource = template.Resources?.[logicalId];
35198
+ if (!resource) return void 0;
35199
+ return readCdkPath(resource) || void 0;
35200
+ }
35201
+ /**
35179
35202
  * Local intrinsic resolver for `IntegrationUri` (and the equivalent
35180
35203
  * `Uri` field on REST v1 Method.Integration). Delegates to the shared
35181
35204
  * `resolveLambdaArnIntrinsic` in `intrinsic-lambda-arn.ts` (extracted in
@@ -37901,39 +37924,74 @@ function groupRoutesByServer(routes) {
37901
37924
  /**
37902
37925
  * Filter the route list to a single API by user-supplied identifier.
37903
37926
  *
37904
- * Matches against both the parent API logical id (HTTP API / REST v1)
37905
- * AND the Function URL's backing-Lambda logical id, so users can pass
37906
- * any of:
37907
- *
37908
- * - The HTTP API logical id (e.g. `MyHttpApi`)
37909
- * - The REST API logical id (e.g. `MyRestApi`)
37910
- * - The Lambda logical id backing a Function URL (e.g. `GoHandler`)
37927
+ * Accepts four input forms matches the rest of the `cdkd local *`
37928
+ * target-resolution family (`local invoke <target>` /
37929
+ * `local run-task <target>`) for consistency:
37930
+ *
37931
+ * 1. **Bare logical id** (`MyHttpApi`) — exact match on the parent
37932
+ * API's logical id, or on the backing Lambda's logical id for
37933
+ * Function URLs.
37934
+ * 2. **Stack-qualified logical id** (`MyStack:MyHttpApi`) — exact
37935
+ * match on `<stackName>:<logicalId>`. Useful in multi-stack apps
37936
+ * where the same bare logical id appears in two stacks.
37937
+ * 3. **CDK Construct path / display path** (`MyStack/MyHttpApi`) —
37938
+ * exact match on the resource's `aws:cdk:path` metadata.
37939
+ * 4. **CDK Construct path prefix** — when the input is a strict
37940
+ * ancestor of the resource's `aws:cdk:path` (i.e.
37941
+ * `cdkPath.startsWith(input + '/')`). Mirrors the prefix rule
37942
+ * `cdkd orphan` uses so an L2 wrapper path resolves to its L1
37943
+ * child (`MyStack/MyHttpApi` matches `MyStack/MyHttpApi/Resource`).
37944
+ *
37945
+ * Routes discovered before this field set was added (or routes where
37946
+ * the synthesized template doesn't carry `aws:cdk:path` metadata —
37947
+ * e.g. hand-rolled `cfn.Resource` defs) silently fall through to the
37948
+ * bare-logical-id-only path so the change is non-breaking.
37911
37949
  *
37912
37950
  * Returns an empty array when no route matches — the caller is
37913
37951
  * responsible for surfacing a "no API matched" error with the list of
37914
37952
  * available identifiers (see {@link availableApiIdentifiers}).
37915
37953
  */
37916
37954
  function filterRoutesByApiIdentifier(routes, identifier) {
37917
- return routes.filter((rwa) => {
37918
- const r = rwa.route;
37919
- if (r.source === "function-url") return r.lambdaLogicalId === identifier;
37920
- return r.apiLogicalId === identifier;
37921
- });
37955
+ return routes.filter((rwa) => routeMatchesIdentifier(rwa.route, identifier));
37956
+ }
37957
+ /**
37958
+ * Predicate behind {@link filterRoutesByApiIdentifier} and
37959
+ * {@link availableApiIdentifiers}'s primary-form selection. Exported
37960
+ * for test coverage only — the production code path goes through
37961
+ * `filterRoutesByApiIdentifier`.
37962
+ */
37963
+ function routeMatchesIdentifier(route, identifier) {
37964
+ const bareId = route.source === "function-url" ? route.lambdaLogicalId : route.apiLogicalId;
37965
+ if (bareId && bareId === identifier) return true;
37966
+ if (route.apiStackName) {
37967
+ if (bareId && identifier === `${route.apiStackName}:${bareId}`) return true;
37968
+ }
37969
+ if (route.apiCdkPath) {
37970
+ if (identifier === route.apiCdkPath) return true;
37971
+ if (route.apiCdkPath.startsWith(`${identifier}/`)) return true;
37972
+ }
37973
+ return false;
37922
37974
  }
37923
37975
  /**
37924
37976
  * Enumerate every distinct API identifier in the route list, in the
37925
37977
  * order they were discovered. Useful for the "available APIs" error
37926
37978
  * message when `--api <id>` doesn't match.
37979
+ *
37980
+ * Returns the **primary form** per API (CDK Construct path when
37981
+ * available, else bare logical id) — the "available identifiers" hint
37982
+ * stays compact while pointing users at the form most likely to round-
37983
+ * trip across rename refactors.
37927
37984
  */
37928
37985
  function availableApiIdentifiers(routes) {
37929
37986
  const seen = /* @__PURE__ */ new Set();
37930
37987
  const out = [];
37931
37988
  for (const rwa of routes) {
37932
37989
  const r = rwa.route;
37933
- const id = r.source === "function-url" ? r.lambdaLogicalId : r.apiLogicalId ?? "<unknown>";
37934
- if (!seen.has(id)) {
37935
- seen.add(id);
37936
- out.push(id);
37990
+ const bareId = r.source === "function-url" ? r.lambdaLogicalId : r.apiLogicalId ?? "<unknown>";
37991
+ const primary = r.apiCdkPath ?? bareId;
37992
+ if (!seen.has(primary)) {
37993
+ seen.add(primary);
37994
+ out.push(primary);
37937
37995
  }
37938
37996
  }
37939
37997
  return out;
@@ -38932,7 +38990,7 @@ function parseDebugPort(raw) {
38932
38990
  * Builder for the `start-api` subcommand. Wired up by `local.ts`.
38933
38991
  */
38934
38992
  function createLocalStartApiCommand() {
38935
- const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers and Cognito User Pool / HTTP v2 JWT authorizers; when JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "Restrict to a single API surface by its logical id (HTTP API / REST API logical id, or the backing Lambda's logical id for Function URLs). When unset, every discovered API gets its own server on its own port (basePort, basePort+1, ... when --port is set; auto-allocated otherwise).")).action(withErrorHandling(localStartApiCommand));
38993
+ const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers and Cognito User Pool / HTTP v2 JWT authorizers; when JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "Restrict to a single API surface. Accepts the bare CDK logical id (e.g. 'MyHttpApi'), the stack-qualified logical id ('MyStack:MyHttpApi'), the full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). For Function URLs, the path forms reference the backing Lambda's aws:cdk:path. When unset, every discovered API gets its own server on its own port (basePort, basePort+1, ... when --port is set; auto-allocated otherwise).")).action(withErrorHandling(localStartApiCommand));
38936
38994
  [
38937
38995
  ...commonOptions,
38938
38996
  ...appOptions,
@@ -41771,7 +41829,7 @@ function reorderArgs(argv) {
41771
41829
  */
41772
41830
  async function main() {
41773
41831
  const program = new Command();
41774
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.94.13");
41832
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.94.15");
41775
41833
  program.addCommand(createBootstrapCommand());
41776
41834
  program.addCommand(createSynthCommand());
41777
41835
  program.addCommand(createListCommand());