@go-to-k/cdkd 0.135.1 → 0.137.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-EtWSTAje.js";
3
- import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as resolveBucketRegion, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as LocalStartServiceError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as AssemblyReader, V as resolveStateBucketWithDefaultAndSource, X as LocalInvokeBuildError, Z as LocalMigrateError, _ as normalizeAwsTagsToCfn, a as withRetry, at as RouteDiscoveryError, b as CloudControlProvider, c as cyan, d as red, et as MissingCdkCliError, f as yellow, ft as normalizeAwsError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceUpdateNotSupportedError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as ProvisioningError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as StackHasActiveImportsError, p as IAMRoleProvider, pt as withErrorHandling, q as CdkdError, r as DeployEngine, rt as ResourceTimeoutError, s as bold, st as StackTerminationProtectionError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as PartialFailureError, u as green, v as resolveExplicitPhysicalId, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-CX1x5ug1.js";
3
+ import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as MIGRATE_TMP_PREFIX, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, J as AssemblyReader, K as findLargeInlineResources, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as CdkdError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as CFN_TEMPLATE_BODY_LIMIT, V as resolveStateBucketWithDefaultAndSource, W as CFN_TEMPLATE_URL_LIMIT, X as resolveBucketRegion, _ as normalizeAwsTagsToCfn, _t as normalizeAwsError, a as withRetry, at as MissingCdkCliError, b as CloudControlProvider, c as cyan, ct as ResourceTimeoutError, d as red, dt as StackHasActiveImportsError, f as yellow, ft as StackTerminationProtectionError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, j as stringifyValue, k as shouldRetainResource, l as gray, lt as ResourceUpdateNotSupportedError, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LocalMigrateError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as PartialFailureError, p as IAMRoleProvider, q as uploadCfnTemplate, r as DeployEngine, rt as LocalStartServiceError, s as bold, st as ProvisioningError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LocalInvokeBuildError, u as green, ut as RouteDiscoveryError, v as resolveExplicitPhysicalId, vt as withErrorHandling, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-DZYchTh6.js";
4
4
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-BF03Alpe.js";
5
5
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
6
- import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
6
+ import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
7
7
  import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
8
8
  import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ListQueueTagsCommand, ListQueuesCommand, QueueDoesNotExist, SQSClient, SetQueueAttributesCommand, TagQueueCommand, UntagQueueCommand } from "@aws-sdk/client-sqs";
9
9
  import { CreateTopicCommand, DeleteTopicCommand, GetSubscriptionAttributesCommand, GetTopicAttributesCommand, ListTagsForResourceCommand, ListTopicsCommand, NotFoundException, SNSClient, SetTopicAttributesCommand, SubscribeCommand, TagResourceCommand, UnsubscribeCommand, UntagResourceCommand } from "@aws-sdk/client-sns";
@@ -65,6 +65,7 @@ import { createServer } from "node:net";
65
65
  import { promisify } from "node:util";
66
66
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
67
67
  import { readFile } from "fs/promises";
68
+ import { WebSocketServer } from "ws";
68
69
  import { createServer as createServer$1 } from "node:http";
69
70
  import { createServer as createServer$2 } from "node:https";
70
71
  import * as chokidar from "chokidar";
@@ -30866,7 +30867,9 @@ async function deployCommand(stacks, options) {
30866
30867
  output: options.output,
30867
30868
  ...options.region && { region: options.region },
30868
30869
  ...options.profile && { profile: options.profile },
30869
- ...Object.keys(context).length > 0 && { context }
30870
+ ...Object.keys(context).length > 0 && { context },
30871
+ stateBucket,
30872
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
30870
30873
  });
30871
30874
  logger.debug(`Found ${allStacks.length} stack(s) in assembly`);
30872
30875
  const stackPatterns = stacks.length > 0 ? stacks : options.stack ? [options.stack] : [];
@@ -31124,7 +31127,9 @@ async function diffCommand(stacks, options) {
31124
31127
  output: options.output,
31125
31128
  ...options.region && { region: options.region },
31126
31129
  ...options.profile && { profile: options.profile },
31127
- ...Object.keys(context).length > 0 && { context }
31130
+ ...Object.keys(context).length > 0 && { context },
31131
+ stateBucket,
31132
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
31128
31133
  });
31129
31134
  logger.info(`Found ${allStacks.length} stack(s) in assembly`);
31130
31135
  const stackPatterns = stacks.length > 0 ? stacks : options.stack ? [options.stack] : [];
@@ -32477,7 +32482,9 @@ async function destroyCommand(stackArgs, options) {
32477
32482
  appStacks = (await synthesizer.synthesize({
32478
32483
  app: appCmd,
32479
32484
  output: options.output || "cdk.out",
32480
- ...Object.keys(context).length > 0 && { context }
32485
+ ...Object.keys(context).length > 0 && { context },
32486
+ stateBucket,
32487
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
32481
32488
  })).stacks.map((s) => ({
32482
32489
  stackName: s.stackName,
32483
32490
  displayName: s.displayName,
@@ -33101,7 +33108,9 @@ async function orphanCommand(pathArgs, options) {
33101
33108
  const resolved = resolveConstructPaths(pathArgs, (await synthesizer.synthesize({
33102
33109
  app: appCmd,
33103
33110
  output: options.output || "cdk.out",
33104
- ...Object.keys(context).length > 0 && { context }
33111
+ ...Object.keys(context).length > 0 && { context },
33112
+ stateBucket,
33113
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
33105
33114
  })).stacks);
33106
33115
  const stackInfo = resolved.stack;
33107
33116
  const orphanLogicalIds = resolved.logicalIds;
@@ -34775,143 +34784,6 @@ const STABLE_TERMINAL_STATUSES = new Set([
34775
34784
  "IMPORT_ROLLBACK_COMPLETE"
34776
34785
  ]);
34777
34786
 
34778
- //#endregion
34779
- //#region src/cli/upload-cfn-template.ts
34780
- /**
34781
- * CloudFormation `TemplateBody` hard limit (51,200 bytes). Templates larger
34782
- * than this cannot be submitted inline and must be uploaded to S3 and
34783
- * referenced via `TemplateURL` instead — see {@link uploadCfnTemplate}.
34784
- */
34785
- const CFN_TEMPLATE_BODY_LIMIT = 51200;
34786
- /**
34787
- * CloudFormation `TemplateURL` hard limit (1 MB / 1,048,576 bytes).
34788
- * Templates larger than this are structurally unsubmittable through any
34789
- * CloudFormation API — no S3 indirection helps. The caller surfaces a
34790
- * pre-flight error pointing the user at template-splitting (nested stacks)
34791
- * or shrinking inline asset payloads (`lambda.Code.fromAsset`).
34792
- */
34793
- const CFN_TEMPLATE_URL_LIMIT = 1048576;
34794
- /**
34795
- * Shared S3 key prefix for transient CFn templates uploaded by `cdkd import
34796
- * --migrate-from-cloudformation` and `cdkd export`. Kept distinct from
34797
- * cdkd's `cdkd/` state prefix so `state list` / `state info` never conflate
34798
- * transient migration artifacts with persisted stack state. The prefix is
34799
- * intentionally human-grep-able — leftovers (if cleanup fails) point
34800
- * straight at the offending stack name.
34801
- *
34802
- * Re-used by both commands so operator-facing audit trails (CloudTrail
34803
- * records of the migrate-tmp uploads) stay consistent across the two
34804
- * flows.
34805
- */
34806
- const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
34807
- /**
34808
- * Upload a CFn template body to the cdkd state bucket and return both a
34809
- * virtual-hosted-style HTTPS URL CloudFormation can fetch via
34810
- * `TemplateURL` and a `cleanup` callback that deletes the object (and
34811
- * destroys the S3 client).
34812
- *
34813
- * The state bucket's actual region is resolved via `GetBucketLocation`
34814
- * (cached per-process) so the upload client and the URL match the
34815
- * bucket's region — the calling CLI's profile region is irrelevant here.
34816
- *
34817
- * Cleanup is the caller's responsibility: invoke `cleanup` in a `finally`
34818
- * around the CFn call. CloudFormation copies the template into its own
34819
- * internal storage during the synchronous `CreateChangeSet` /
34820
- * `UpdateStack` API call, so the S3 object is no longer needed after that
34821
- * call returns (success or failure).
34822
- *
34823
- * Shared between `cdkd import --migrate-from-cloudformation` (via
34824
- * `retire-cfn-stack.ts`) and `cdkd export` (via `commands/export.ts`) so
34825
- * the upload + cleanup contract is single-sourced.
34826
- */
34827
- async function uploadCfnTemplate(args) {
34828
- const { bucket, body, stackName, format, s3ClientOpts } = args;
34829
- const region = await resolveBucketRegion(bucket, {
34830
- ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
34831
- ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
34832
- });
34833
- const s3 = new S3Client({
34834
- region,
34835
- ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
34836
- ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
34837
- });
34838
- const ext = format === "yaml" ? "yaml" : "json";
34839
- const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
34840
- const key = `${MIGRATE_TMP_PREFIX}/${stackName}/${Date.now()}.${ext}`;
34841
- try {
34842
- await s3.send(new PutObjectCommand({
34843
- Bucket: bucket,
34844
- Key: key,
34845
- Body: body,
34846
- ContentType: contentType
34847
- }));
34848
- } catch (err) {
34849
- s3.destroy();
34850
- throw err;
34851
- }
34852
- const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
34853
- const cleanup = async () => {
34854
- try {
34855
- await s3.send(new DeleteObjectCommand({
34856
- Bucket: bucket,
34857
- Key: key
34858
- }));
34859
- } finally {
34860
- s3.destroy();
34861
- }
34862
- };
34863
- return {
34864
- url,
34865
- cleanup
34866
- };
34867
- }
34868
- /**
34869
- * Threshold (in bytes) above which a single resource's serialized
34870
- * `Properties` block is considered an "inline payload" worth surfacing as
34871
- * a contributor to a template that exceeds the 1 MB CFn `TemplateURL`
34872
- * ceiling. 4 KB matches the typical inline `Code.ZipFile` Lambda payload
34873
- * that pushes a multi-resource CDK app over the wire-format limit.
34874
- */
34875
- const LARGE_INLINE_RESOURCE_THRESHOLD = 4096;
34876
- /**
34877
- * Walk a CFn template and surface every resource whose serialized
34878
- * `Properties` block exceeds {@link LARGE_INLINE_RESOURCE_THRESHOLD}.
34879
- * Used to build the actionable "offending resources" list in the
34880
- * pre-flight error when a template exceeds the 1 MB `TemplateURL`
34881
- * ceiling — typical culprits are inline `Code.ZipFile` Lambdas, inline
34882
- * StepFunctions definitions, or large `AWS::CloudFormation::Stack`
34883
- * bodies.
34884
- *
34885
- * Returns entries sorted by `approxBytes` descending so the user sees
34886
- * the biggest contributor first. A non-CFn-template input (no
34887
- * `Resources` object) returns an empty array.
34888
- */
34889
- function findLargeInlineResources(template, threshold = LARGE_INLINE_RESOURCE_THRESHOLD) {
34890
- const result = [];
34891
- const resources = template["Resources"];
34892
- if (!resources || typeof resources !== "object" || Array.isArray(resources)) return result;
34893
- for (const [logicalId, resource] of Object.entries(resources)) {
34894
- if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
34895
- const r = resource;
34896
- const resourceType = typeof r["Type"] === "string" ? r["Type"] : "<unknown>";
34897
- const properties = r["Properties"];
34898
- if (properties === void 0 || properties === null) continue;
34899
- let approxBytes;
34900
- try {
34901
- approxBytes = JSON.stringify(properties).length;
34902
- } catch {
34903
- continue;
34904
- }
34905
- if (approxBytes >= threshold) result.push({
34906
- logicalId,
34907
- resourceType,
34908
- approxBytes
34909
- });
34910
- }
34911
- result.sort((a, b) => b.approxBytes - a.approxBytes);
34912
- return result;
34913
- }
34914
-
34915
34787
  //#endregion
34916
34788
  //#region src/cli/yaml-cfn.ts
34917
34789
  /**
@@ -35498,7 +35370,9 @@ async function importCommand(stackArg, options) {
35498
35370
  const result = await synthesizer.synthesize({
35499
35371
  app: appCmd,
35500
35372
  output: options.output || "cdk.out",
35501
- ...Object.keys(context).length > 0 && { context }
35373
+ ...Object.keys(context).length > 0 && { context },
35374
+ stateBucket,
35375
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
35502
35376
  });
35503
35377
  let stackInfo;
35504
35378
  if (stackArg) {
@@ -38839,6 +38713,7 @@ async function runDetached(opts) {
38839
38713
  if (opts.name) args.push("--name", opts.name);
38840
38714
  if (opts.network) args.push("--network", opts.network);
38841
38715
  if (opts.platform) args.push("--platform", opts.platform);
38716
+ if (opts.extraHosts) for (const entry of opts.extraHosts) args.push("--add-host", `${entry.host}:${entry.ip}`);
38842
38717
  const host = opts.host ?? "127.0.0.1";
38843
38718
  args.push("-p", `${host}:${opts.hostPort}:8080`);
38844
38719
  if (opts.debugPort !== void 0) args.push("-p", `${host}:${opts.debugPort}:${opts.debugPort}`);
@@ -40488,13 +40363,13 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
40488
40363
  const integration = props["Integration"];
40489
40364
  if (!integration) throw new Error(`${stackName}/${logicalId} (AWS::ApiGateway::Method): missing Integration property`);
40490
40365
  const restApiId = props["RestApiId"];
40491
- const restApiLogicalId = pickRefLogicalId$2(restApiId);
40366
+ const restApiLogicalId = pickRefLogicalId$3(restApiId);
40492
40367
  if (!restApiLogicalId) throw new Error(`${stackName}/${logicalId} (AWS::ApiGateway::Method): RestApiId must be a { Ref: '...' } reference (got ${shortJson$1(restApiId)}).`);
40493
40368
  const resourceId = props["ResourceId"];
40494
40369
  const path = buildRestV1Path(resourceId, restApiLogicalId, template, stackName, logicalId);
40495
40370
  const httpMethod = stringifyValue(props["HttpMethod"] ?? "ANY");
40496
40371
  const stage = pickRestV1Stage(restApiLogicalId, template);
40497
- const restApiCdkPath = readApiCdkPath(restApiLogicalId, template);
40372
+ const restApiCdkPath = readApiCdkPath$1(restApiLogicalId, template);
40498
40373
  const baseRoute = {
40499
40374
  source: "rest-v1",
40500
40375
  apiVersion: "v1",
@@ -40848,7 +40723,7 @@ function buildRestV1Path(resourceIdIntrinsic, restApiLogicalId, template, stackN
40848
40723
  if (Array.isArray(arg) && arg.length === 2 && arg[1] === "RootResourceId") return "/";
40849
40724
  }
40850
40725
  }
40851
- const resourceLogicalId = pickRefLogicalId$2(resourceIdIntrinsic);
40726
+ const resourceLogicalId = pickRefLogicalId$3(resourceIdIntrinsic);
40852
40727
  if (!resourceLogicalId) throw new Error(`${stackName}/${methodLogicalId}: ResourceId must be { Ref: '...' } or { 'Fn::GetAtt': [..., 'RootResourceId'] } (got ${shortJson$1(resourceIdIntrinsic)}).`);
40853
40728
  const segments = [];
40854
40729
  const visited = /* @__PURE__ */ new Set();
@@ -40868,7 +40743,7 @@ function buildRestV1Path(resourceIdIntrinsic, restApiLogicalId, template, stackN
40868
40743
  const arg = parentId["Fn::GetAtt"];
40869
40744
  if (Array.isArray(arg) && arg[1] === "RootResourceId") break;
40870
40745
  }
40871
- cursor = pickRefLogicalId$2(parentId) ?? void 0;
40746
+ cursor = pickRefLogicalId$3(parentId) ?? void 0;
40872
40747
  }
40873
40748
  return "/" + segments.join("/");
40874
40749
  }
@@ -40883,7 +40758,7 @@ function pickRestV1Stage(restApiLogicalId, template) {
40883
40758
  for (const [, resource] of Object.entries(resources)) {
40884
40759
  if (resource.Type !== "AWS::ApiGateway::Stage") continue;
40885
40760
  const props = resource.Properties ?? {};
40886
- if (pickRefLogicalId$2(props["RestApiId"]) === restApiLogicalId) {
40761
+ if (pickRefLogicalId$3(props["RestApiId"]) === restApiLogicalId) {
40887
40762
  const stageName = props["StageName"];
40888
40763
  if (typeof stageName === "string") return stageName;
40889
40764
  }
@@ -40902,26 +40777,14 @@ function pickRestV1Stage(restApiLogicalId, template) {
40902
40777
  function discoverHttpApiRoute(logicalId, resource, template, stackName) {
40903
40778
  const props = resource.Properties ?? {};
40904
40779
  const apiId = props["ApiId"];
40905
- const apiLogicalId = pickRefLogicalId$2(apiId);
40780
+ const apiLogicalId = pickRefLogicalId$3(apiId);
40906
40781
  if (!apiLogicalId) throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): ApiId must be { Ref: '...' } (got ${shortJson$1(apiId)}).`);
40907
40782
  const routeKey = props["RouteKey"];
40908
40783
  if (typeof routeKey !== "string" || routeKey.length === 0) throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): RouteKey must be a string`);
40909
- const apiCdkPath = readApiCdkPath(apiLogicalId, template);
40784
+ const apiCdkPath = readApiCdkPath$1(apiLogicalId, template);
40910
40785
  const apiResource = template.Resources?.[apiLogicalId];
40911
40786
  if (apiResource?.Type === "AWS::ApiGatewayV2::Api") {
40912
- if ((apiResource.Properties ?? {})["ProtocolType"] === "WEBSOCKET") return [{
40913
- method: "ANY",
40914
- pathPattern: routeKey,
40915
- lambdaLogicalId: "",
40916
- source: "http-api",
40917
- apiVersion: "v2",
40918
- stage: "$default",
40919
- apiLogicalId,
40920
- apiStackName: stackName,
40921
- ...apiCdkPath !== void 0 && { apiCdkPath },
40922
- declaredAt: `${stackName}/${logicalId}`,
40923
- unsupported: { reason: `${stackName}/${logicalId}: WebSocket APIs are not supported in cdkd local start-api.` }
40924
- }];
40787
+ if ((apiResource.Properties ?? {})["ProtocolType"] === "WEBSOCKET") return [];
40925
40788
  }
40926
40789
  const { method, pathPattern } = parseRouteKey(routeKey);
40927
40790
  const baseRoute = {
@@ -41023,7 +40886,7 @@ function discoverFunctionUrl(logicalId, resource, template, stackName) {
41023
40886
  const arnOutcome = resolveLambdaArnOutcome(targetArn);
41024
40887
  if (arnOutcome.kind === "unsupported") throw new Error(`${stackName}/${logicalId}.TargetFunctionArn: ${arnOutcome.detail} (got ${shortJson$1(targetArn)}).`);
41025
40888
  const lambdaLogicalId = arnOutcome.logicalId;
41026
- const lambdaCdkPath = readApiCdkPath(lambdaLogicalId, template);
40889
+ const lambdaCdkPath = readApiCdkPath$1(lambdaLogicalId, template);
41027
40890
  const baseRoute = {
41028
40891
  method: "ANY",
41029
40892
  pathPattern: "/{proxy+}",
@@ -41060,7 +40923,7 @@ function discoverFunctionUrl(logicalId, resource, template, stackName) {
41060
40923
  * metadata isn't set. Hides the "may be missing for a hand-rolled
41061
40924
  * `cfn.Resource`" branch from every call site.
41062
40925
  */
41063
- function readApiCdkPath(logicalId, template) {
40926
+ function readApiCdkPath$1(logicalId, template) {
41064
40927
  const resource = template.Resources?.[logicalId];
41065
40928
  if (!resource) return void 0;
41066
40929
  return readCdkPath(resource) || void 0;
@@ -41127,11 +40990,11 @@ function parseHttpApiTargetIntegration(target, location) {
41127
40990
  const sep = join[0];
41128
40991
  const parts = join[1];
41129
40992
  if (sep === "/" && parts.length === 2 && parts[0] === "integrations") {
41130
- const ref = pickRefLogicalId$2(parts[1]);
40993
+ const ref = pickRefLogicalId$3(parts[1]);
41131
40994
  if (ref) return ref;
41132
40995
  }
41133
40996
  if (sep === "" && parts.length === 2 && parts[0] === "integrations/") {
41134
- const ref = pickRefLogicalId$2(parts[1]);
40997
+ const ref = pickRefLogicalId$3(parts[1]);
41135
40998
  if (ref) return ref;
41136
40999
  }
41137
41000
  }
@@ -41151,7 +41014,7 @@ function parseHttpApiTargetIntegration(target, location) {
41151
41014
  if (m) {
41152
41015
  const bound = bindings[m[1]];
41153
41016
  if (bound !== void 0) {
41154
- const ref = pickRefLogicalId$2(bound);
41017
+ const ref = pickRefLogicalId$3(bound);
41155
41018
  if (ref) return ref;
41156
41019
  }
41157
41020
  }
@@ -41180,7 +41043,7 @@ function parseRouteKey(routeKey) {
41180
41043
  * If `value` is a `{ Ref: <string> }` intrinsic, return the referenced
41181
41044
  * logical ID. Otherwise return `null`.
41182
41045
  */
41183
- function pickRefLogicalId$2(value) {
41046
+ function pickRefLogicalId$3(value) {
41184
41047
  if (value && typeof value === "object" && !Array.isArray(value)) {
41185
41048
  const ref = value["Ref"];
41186
41049
  if (typeof ref === "string") return ref;
@@ -41200,6 +41063,983 @@ function shortJson$1(value) {
41200
41063
  }
41201
41064
  }
41202
41065
 
41066
+ //#endregion
41067
+ //#region src/local/websocket-route-discovery.ts
41068
+ const DEFAULT_ROUTE_SELECTION_EXPRESSION = "$request.body.action";
41069
+ const DEFAULT_STAGE = "local";
41070
+ /**
41071
+ * Walk every synthesized stack and produce one {@link DiscoveredWebSocketApi}
41072
+ * per WebSocket API found. Errors per API are aggregated and surfaced
41073
+ * as a single {@link RouteDiscoveryError} (matches the HTTP-side
41074
+ * discovery behavior — a single malformed API shouldn't abort the
41075
+ * server boot for sibling APIs).
41076
+ *
41077
+ * Resolution chain:
41078
+ * 1. Find each `AWS::ApiGatewayV2::Api` with `ProtocolType: WEBSOCKET`.
41079
+ * 2. Validate `RouteSelectionExpression` (only `$request.body.<key>`
41080
+ * forms supported in v1).
41081
+ * 3. Resolve attached Stage (first Stage referencing this API).
41082
+ * 4. Walk every `AWS::ApiGatewayV2::Route` referencing this API,
41083
+ * resolve each route's Target → Integration → IntegrationUri →
41084
+ * Lambda logical ID via the shared `resolveLambdaArnIntrinsic`
41085
+ * helper (handles every CFn intrinsic shape CDK emits).
41086
+ */
41087
+ function discoverWebSocketApis(stacks) {
41088
+ const apis = [];
41089
+ const errors = [];
41090
+ for (const stack of stacks) {
41091
+ const template = stack.template;
41092
+ const resources = template.Resources ?? {};
41093
+ for (const [logicalId, resource] of Object.entries(resources)) {
41094
+ if (resource.Type !== "AWS::ApiGatewayV2::Api") continue;
41095
+ if ((resource.Properties ?? {})["ProtocolType"] !== "WEBSOCKET") continue;
41096
+ try {
41097
+ apis.push(discoverOneApi(logicalId, resource, template, stack.stackName));
41098
+ } catch (err) {
41099
+ errors.push(err instanceof Error ? err.message : String(err));
41100
+ }
41101
+ }
41102
+ }
41103
+ return {
41104
+ apis,
41105
+ errors
41106
+ };
41107
+ }
41108
+ function discoverOneApi(logicalId, resource, template, stackName) {
41109
+ const props = resource.Properties ?? {};
41110
+ const declaredAt = `${stackName}/${logicalId}`;
41111
+ const rawSelection = props["RouteSelectionExpression"];
41112
+ const routeSelectionExpression = typeof rawSelection === "string" && rawSelection.length > 0 ? rawSelection : DEFAULT_ROUTE_SELECTION_EXPRESSION;
41113
+ assertSupportedSelectionExpression(routeSelectionExpression, declaredAt);
41114
+ const stage = pickStage$1(logicalId, template);
41115
+ const apiCdkPath = readApiCdkPath(logicalId, template);
41116
+ const routes = collectRoutesForApi(logicalId, template, stackName);
41117
+ if (routes.length === 0) throw new Error(`${declaredAt}: WebSocket API has no AWS::ApiGatewayV2::Route children — at least one route (typically '$connect') is required to dispatch.`);
41118
+ const authRoutes = collectAuthRoutesForApi(logicalId, template, stackName);
41119
+ const unsupported = authRoutes.length > 0 ? { reason: `WebSocket API requires authorizer support, which cdkd v1 does not emulate. Affected route(s): ${authRoutes.map((r) => `${r.routeKey} [AuthorizationType=${r.authorizationType}]`).join(", ")}. The API will be discovered but no upgrade requests will be accepted on this server.` } : void 0;
41120
+ return {
41121
+ apiLogicalId: logicalId,
41122
+ apiStackName: stackName,
41123
+ declaredAt,
41124
+ ...apiCdkPath !== "" && { apiCdkPath },
41125
+ routeSelectionExpression,
41126
+ stage,
41127
+ routes,
41128
+ ...unsupported !== void 0 && { unsupported }
41129
+ };
41130
+ }
41131
+ /**
41132
+ * Scan the synthesized template for every `AWS::ApiGatewayV2::Route`
41133
+ * referencing the given WebSocket API, returning the subset whose
41134
+ * `AuthorizationType` is set to anything other than `NONE` (the
41135
+ * AWS-default when omitted). Used by {@link discoverOneApi} to tag
41136
+ * the parent API as unsupported when v1's no-authorizer emulation
41137
+ * gap would otherwise let unauthenticated clients through.
41138
+ */
41139
+ function collectAuthRoutesForApi(apiLogicalId, template, _stackName) {
41140
+ const resources = template.Resources ?? {};
41141
+ const result = [];
41142
+ for (const [, resource] of Object.entries(resources)) {
41143
+ if (resource.Type !== "AWS::ApiGatewayV2::Route") continue;
41144
+ const props = resource.Properties ?? {};
41145
+ if (pickRefLogicalId$2(props["ApiId"]) !== apiLogicalId) continue;
41146
+ const authType = props["AuthorizationType"];
41147
+ if (typeof authType !== "string" || authType.length === 0) continue;
41148
+ if (authType === "NONE") continue;
41149
+ const routeKey = props["RouteKey"];
41150
+ result.push({
41151
+ routeKey: typeof routeKey === "string" ? routeKey : "<unknown>",
41152
+ authorizationType: authType
41153
+ });
41154
+ }
41155
+ return result;
41156
+ }
41157
+ /**
41158
+ * `$request.body.<key>` is the AWS-canonical shape and the only one
41159
+ * v1 supports. Allow nested dot access (`$request.body.action.subKey`)
41160
+ * — real CDK chat apps sometimes use this for protocol versioning.
41161
+ *
41162
+ * Reject array-index access (`$request.body.items[0]`), filter
41163
+ * expressions, header / context selections — these would require a
41164
+ * fuller JSONPath / VTL evaluator and are out of scope for v1.
41165
+ */
41166
+ function assertSupportedSelectionExpression(expr, declaredAt) {
41167
+ if (!/^\$request\.body(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(expr)) throw new Error(`${declaredAt}: RouteSelectionExpression '${expr}' is not supported in cdkd local start-api v1 — only '$request.body.<key>' shapes (optionally nested via dots) are recognized. File a follow-up issue if you need '$request.header.X' / '$context.X' / array-index access.`);
41168
+ }
41169
+ /**
41170
+ * Parse a `$request.body.x.y` selection expression into the JSON-path
41171
+ * tokens after `$request.body`. Returns `['x', 'y']` for the example
41172
+ * above. Used at message-dispatch time to walk the parsed message body.
41173
+ */
41174
+ function parseSelectionExpressionPath(expr) {
41175
+ const m = /^\$request\.body\.(.+)$/.exec(expr);
41176
+ if (!m) return [];
41177
+ return m[1].split(".");
41178
+ }
41179
+ /**
41180
+ * Pick the first `AWS::ApiGatewayV2::Stage` referencing the API. CDK's
41181
+ * `apigatewayv2.WebSocketStage` always emits one; the fallback to
41182
+ * `'local'` handles hand-rolled templates without a Stage.
41183
+ */
41184
+ function pickStage$1(apiLogicalId, template) {
41185
+ const resources = template.Resources ?? {};
41186
+ for (const [, resource] of Object.entries(resources)) {
41187
+ if (resource.Type !== "AWS::ApiGatewayV2::Stage") continue;
41188
+ const props = resource.Properties ?? {};
41189
+ if (pickRefLogicalId$2(props["ApiId"]) === apiLogicalId) {
41190
+ const stageName = props["StageName"];
41191
+ if (typeof stageName === "string" && stageName.length > 0) return stageName;
41192
+ }
41193
+ }
41194
+ return DEFAULT_STAGE;
41195
+ }
41196
+ /**
41197
+ * Walk every `AWS::ApiGatewayV2::Route` and resolve each one whose
41198
+ * parent `ApiId` Ref matches the WebSocket API. Per-route failures
41199
+ * abort the API's discovery (a partial route map would silently
41200
+ * disable some routes — better to fail fast and let the user fix the
41201
+ * template).
41202
+ */
41203
+ function collectRoutesForApi(apiLogicalId, template, stackName) {
41204
+ const resources = template.Resources ?? {};
41205
+ const result = [];
41206
+ const seenKeys = /* @__PURE__ */ new Set();
41207
+ for (const [routeLogicalId, resource] of Object.entries(resources)) {
41208
+ if (resource.Type !== "AWS::ApiGatewayV2::Route") continue;
41209
+ const props = resource.Properties ?? {};
41210
+ if (pickRefLogicalId$2(props["ApiId"]) !== apiLogicalId) continue;
41211
+ const declaredAt = `${stackName}/${routeLogicalId}`;
41212
+ const routeKey = props["RouteKey"];
41213
+ if (typeof routeKey !== "string" || routeKey.length === 0) throw new Error(`${declaredAt}: RouteKey must be a non-empty string.`);
41214
+ if (seenKeys.has(routeKey)) throw new Error(`${declaredAt}: WebSocket API has duplicate RouteKey '${routeKey}' — each RouteKey may appear at most once per API.`);
41215
+ seenKeys.add(routeKey);
41216
+ const targetLogicalId = parseRouteTarget(props["Target"], declaredAt);
41217
+ const integration = resources[targetLogicalId];
41218
+ if (!integration || integration.Type !== "AWS::ApiGatewayV2::Integration") throw new Error(`${declaredAt}: Target points at '${targetLogicalId}' which is not an AWS::ApiGatewayV2::Integration.`);
41219
+ const integrationProps = integration.Properties ?? {};
41220
+ const integrationType = integrationProps["IntegrationType"];
41221
+ if (integrationType !== "AWS_PROXY") throw new Error(`${declaredAt}: WebSocket route IntegrationType '${String(integrationType)}' is not supported in cdkd local start-api v1 — only AWS_PROXY (Lambda) integrations are emulated.`);
41222
+ const arnOutcome = resolveLambdaArnIntrinsic(integrationProps["IntegrationUri"]);
41223
+ if (arnOutcome.kind === "unsupported") throw new Error(`${stackName}/${targetLogicalId}.IntegrationUri: ${arnOutcome.detail} — WebSocket routes must point at a same-template Lambda.`);
41224
+ result.push({
41225
+ routeKey,
41226
+ targetLambdaLogicalId: arnOutcome.logicalId,
41227
+ lambdaStackName: stackName,
41228
+ declaredAt
41229
+ });
41230
+ }
41231
+ return result;
41232
+ }
41233
+ /**
41234
+ * WebSocket Routes use the same `Target: 'integrations/<id>'` shape as
41235
+ * HTTP API v2 Routes. We accept the same five forms documented in
41236
+ * `route-discovery.ts:parseHttpApiTargetIntegration` — literal string,
41237
+ * two `Fn::Join` shapes, two `Fn::Sub` shapes.
41238
+ *
41239
+ * Implementation note: we intentionally duplicate the parser rather
41240
+ * than reach into `route-discovery.ts` because that module is in flux
41241
+ * (`unsupported` / `mockCors` shapes; HTTP-specific). When the two
41242
+ * parsers grow apart, the WebSocket one only needs to track the AWS
41243
+ * WebSocket-side shape — which has been stable since 2018.
41244
+ */
41245
+ function parseRouteTarget(target, location) {
41246
+ if (typeof target === "string") {
41247
+ const m = /^integrations\/(.+)$/.exec(target);
41248
+ if (m) return m[1];
41249
+ throw new Error(`${location}: literal Target '${target}' must start with 'integrations/'.`);
41250
+ }
41251
+ if (target && typeof target === "object" && !Array.isArray(target)) {
41252
+ const obj = target;
41253
+ const join = obj["Fn::Join"];
41254
+ if (Array.isArray(join) && join.length === 2 && Array.isArray(join[1])) {
41255
+ const sep = join[0];
41256
+ const parts = join[1];
41257
+ if (sep === "/" && parts.length === 2 && parts[0] === "integrations") {
41258
+ const ref = pickRefLogicalId$2(parts[1]);
41259
+ if (ref) return ref;
41260
+ }
41261
+ if (sep === "" && parts.length === 2 && parts[0] === "integrations/") {
41262
+ const ref = pickRefLogicalId$2(parts[1]);
41263
+ if (ref) return ref;
41264
+ }
41265
+ }
41266
+ if ("Fn::Sub" in obj) {
41267
+ const sub = obj["Fn::Sub"];
41268
+ const prefix = "integrations/";
41269
+ if (typeof sub === "string") {
41270
+ const m = new RegExp(`^${prefix}\\$\\{([^}]+)\\}$`).exec(sub);
41271
+ if (m) {
41272
+ const placeholder = m[1];
41273
+ if (!placeholder.includes(".")) return placeholder;
41274
+ }
41275
+ }
41276
+ if (Array.isArray(sub) && sub.length === 2 && typeof sub[0] === "string" && sub[1] !== null && typeof sub[1] === "object" && !Array.isArray(sub[1])) {
41277
+ const template = sub[0];
41278
+ const bindings = sub[1];
41279
+ const m = new RegExp(`^${prefix}\\$\\{([^}]+)\\}$`).exec(template);
41280
+ if (m) {
41281
+ const bound = bindings[m[1]];
41282
+ if (bound !== void 0) {
41283
+ const ref = pickRefLogicalId$2(bound);
41284
+ if (ref) return ref;
41285
+ }
41286
+ }
41287
+ }
41288
+ }
41289
+ }
41290
+ throw new Error(`${location}: Target must be 'integrations/<id>' literal, Fn::Join with the documented shapes, or Fn::Sub with an 'integrations/\${...}' template.`);
41291
+ }
41292
+ function pickRefLogicalId$2(value) {
41293
+ if (value && typeof value === "object" && !Array.isArray(value)) {
41294
+ const ref = value["Ref"];
41295
+ if (typeof ref === "string") return ref;
41296
+ }
41297
+ return null;
41298
+ }
41299
+ function readApiCdkPath(logicalId, template) {
41300
+ const resource = template.Resources?.[logicalId];
41301
+ if (!resource) return "";
41302
+ return readCdkPath(resource);
41303
+ }
41304
+
41305
+ //#endregion
41306
+ //#region src/local/websocket-event.ts
41307
+ const MOCK_DOMAIN_NAME$1 = "localhost";
41308
+ const MOCK_API_ID$1 = "local";
41309
+ /**
41310
+ * Build a request-context block shared across all three event types.
41311
+ * `eventType` / `routeKey` are passed in by the per-event caller; the
41312
+ * shared block produces fresh `requestId` / `extendedRequestId` per
41313
+ * event (matching AWS-deployed behavior — these are NOT stable across
41314
+ * events on the same connection).
41315
+ */
41316
+ function buildRequestContext(routeKey, eventType, connectionId, connectedAt, stage, snapshot) {
41317
+ const now = Date.now();
41318
+ return {
41319
+ routeKey,
41320
+ eventType,
41321
+ connectionId,
41322
+ extendedRequestId: randomUUID(),
41323
+ requestTime: formatRequestTime$1(now),
41324
+ requestTimeEpoch: now,
41325
+ messageDirection: "IN",
41326
+ stage,
41327
+ connectedAt,
41328
+ requestId: randomUUID(),
41329
+ domainName: MOCK_DOMAIN_NAME$1,
41330
+ apiId: MOCK_API_ID$1,
41331
+ authorizer: null,
41332
+ identity: {
41333
+ sourceIp: snapshot.sourceIp ?? "127.0.0.1",
41334
+ userAgent: snapshot.userAgent ?? ""
41335
+ }
41336
+ };
41337
+ }
41338
+ /**
41339
+ * Build the `$connect` event. AWS WebSocket APIs fire `$connect` ONCE
41340
+ * per client connection. Handler returns `{statusCode: 200}` to allow
41341
+ * the connection, anything else (or throws) to deny — cdkd matches the
41342
+ * deployed behavior by checking the response in the caller.
41343
+ */
41344
+ function buildConnectEvent(opts) {
41345
+ const headers = normalizeHeaders(opts.snapshot.headers);
41346
+ const multiValueHeaders = lowercaseMultiValueHeaders(opts.snapshot.headers);
41347
+ return {
41348
+ ...headers !== void 0 && { headers },
41349
+ ...multiValueHeaders !== void 0 && { multiValueHeaders },
41350
+ queryStringParameters: opts.snapshot.queryStringParameters ?? null,
41351
+ multiValueQueryStringParameters: opts.snapshot.multiValueQueryStringParameters ?? null,
41352
+ requestContext: { ...buildRequestContext("$connect", "CONNECT", opts.connectionId, opts.connectedAt, opts.stage, opts.snapshot) },
41353
+ isBase64Encoded: false,
41354
+ body: ""
41355
+ };
41356
+ }
41357
+ /**
41358
+ * Build a MESSAGE event. Fires for every frame the client sends. The
41359
+ * route the API dispatches to is resolved upstream by the route
41360
+ * selection-expression layer; the resolved `routeKey` (`$default` or
41361
+ * a custom string) lands on `requestContext.routeKey`.
41362
+ */
41363
+ function buildMessageEvent(opts) {
41364
+ return {
41365
+ requestContext: {
41366
+ ...buildRequestContext(opts.routeKey, "MESSAGE", opts.connectionId, opts.connectedAt, opts.stage, opts.snapshot),
41367
+ messageId: randomUUID()
41368
+ },
41369
+ isBase64Encoded: opts.isBase64Encoded,
41370
+ body: opts.body
41371
+ };
41372
+ }
41373
+ /**
41374
+ * Build the `$disconnect` event. Fires when the WebSocket closes from
41375
+ * either side (client / server / abnormal). The Lambda's response is
41376
+ * ignored (the socket is already gone); AWS still invokes the handler
41377
+ * for cleanup / logging side effects.
41378
+ *
41379
+ * `disconnectStatusCode` / `disconnectReason` are taken from the
41380
+ * WebSocket close frame (RFC 6455 §7.1.5 — close codes such as 1000
41381
+ * normal / 1001 going-away / 1008 policy-violation).
41382
+ */
41383
+ function buildDisconnectEvent(opts) {
41384
+ return {
41385
+ requestContext: {
41386
+ ...buildRequestContext("$disconnect", "DISCONNECT", opts.connectionId, opts.connectedAt, opts.stage, opts.snapshot),
41387
+ ...opts.disconnectStatusCode !== void 0 && { disconnectStatusCode: opts.disconnectStatusCode },
41388
+ ...opts.disconnectReason !== void 0 && { disconnectReason: opts.disconnectReason }
41389
+ },
41390
+ isBase64Encoded: false,
41391
+ body: ""
41392
+ };
41393
+ }
41394
+ /**
41395
+ * Format a timestamp in the AWS-canonical `dd/MMM/yyyy:HH:mm:ss +0000`
41396
+ * shape that AWS API Gateway emits on `requestContext.requestTime`.
41397
+ * Always UTC (matches AWS-deployed behavior, which is region-independent).
41398
+ */
41399
+ function formatRequestTime$1(epochMs) {
41400
+ const d = new Date(epochMs);
41401
+ return `${String(d.getUTCDate()).padStart(2, "0")}/${[
41402
+ "Jan",
41403
+ "Feb",
41404
+ "Mar",
41405
+ "Apr",
41406
+ "May",
41407
+ "Jun",
41408
+ "Jul",
41409
+ "Aug",
41410
+ "Sep",
41411
+ "Oct",
41412
+ "Nov",
41413
+ "Dec"
41414
+ ][d.getUTCMonth()]}/${d.getUTCFullYear()}:${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")} +0000`;
41415
+ }
41416
+ /**
41417
+ * Lowercase header keys, comma-join duplicates per AWS spec.
41418
+ */
41419
+ function normalizeHeaders(headers) {
41420
+ const out = {};
41421
+ let any = false;
41422
+ for (const [name, values] of Object.entries(headers)) {
41423
+ if (values.length === 0) continue;
41424
+ out[name.toLowerCase()] = values.join(",");
41425
+ any = true;
41426
+ }
41427
+ return any ? out : void 0;
41428
+ }
41429
+ /**
41430
+ * Lowercase header keys, preserve multi-value array shape.
41431
+ */
41432
+ function lowercaseMultiValueHeaders(headers) {
41433
+ const out = {};
41434
+ let any = false;
41435
+ for (const [name, values] of Object.entries(headers)) {
41436
+ if (values.length === 0) continue;
41437
+ out[name.toLowerCase()] = [...values];
41438
+ any = true;
41439
+ }
41440
+ return any ? out : void 0;
41441
+ }
41442
+
41443
+ //#endregion
41444
+ //#region src/local/websocket-mgmt-api.ts
41445
+ /**
41446
+ * `Map<connectionId, ConnectionRegistryEntry>` wrapper with type-safe
41447
+ * accessors. Lookups by connectionId stay O(1).
41448
+ */
41449
+ var ConnectionRegistry = class {
41450
+ entries = /* @__PURE__ */ new Map();
41451
+ register(entry) {
41452
+ this.entries.set(entry.connectionId, entry);
41453
+ }
41454
+ unregister(connectionId) {
41455
+ const entry = this.entries.get(connectionId);
41456
+ if (entry) this.entries.delete(connectionId);
41457
+ return entry;
41458
+ }
41459
+ get(connectionId) {
41460
+ return this.entries.get(connectionId);
41461
+ }
41462
+ size() {
41463
+ return this.entries.size;
41464
+ }
41465
+ /**
41466
+ * Snapshot the live entries (for diagnostics / shutdown drain).
41467
+ * Returns a fresh array so the caller can iterate without ownership
41468
+ * concerns over the underlying Map.
41469
+ */
41470
+ list() {
41471
+ return Array.from(this.entries.values());
41472
+ }
41473
+ clear() {
41474
+ this.entries.clear();
41475
+ }
41476
+ };
41477
+ /**
41478
+ * Match the request URL against the `@connections` endpoint family.
41479
+ * Returns the parsed connectionId on match, `null` otherwise.
41480
+ *
41481
+ * AWS reserves `$` / `@` for control planes so the path prefix
41482
+ * `/@connections/` can never collide with user-declared routes.
41483
+ *
41484
+ * Accepted shapes:
41485
+ * - `/@connections/<id>` (AWS-deployed historical form — supported
41486
+ * for backward compatibility with handlers that construct URLs
41487
+ * manually).
41488
+ * - `/<stage>/@connections/<id>` (AWS-docs-canonical form — the
41489
+ * deployed apigatewaymanagementapi endpoint URL is
41490
+ * `https://<api-id>.execute-api.<region>.amazonaws.com/<stage>`,
41491
+ * so SDK-built clients call `POST /<stage>/@connections/<id>`).
41492
+ *
41493
+ * The optional `<stage>` segment matches AWS's deployed URL exactly —
41494
+ * any non-slash sequence preceding `/@connections/`. The stage value
41495
+ * itself is intentionally NOT validated against the per-API configured
41496
+ * stage name: cdkd is a local-dev tool, not a security boundary, and
41497
+ * an aggressive stage check would just trip on misconfigured handlers
41498
+ * without adding any real protection.
41499
+ */
41500
+ function parseConnectionsPath(url) {
41501
+ const pathOnly = url.split("?", 1)[0];
41502
+ const m = /^\/(?:[^/]+\/)?@connections\/([^/]+)\/?$/.exec(pathOnly);
41503
+ if (!m) return null;
41504
+ const decoded = safeDecodeURIComponent(m[1]);
41505
+ if (decoded === null) return null;
41506
+ return { connectionId: decoded };
41507
+ }
41508
+ /**
41509
+ * `decodeURIComponent` throws `URIError` on malformed input
41510
+ * (`%`-escape with non-hex tail). We treat that as a not-found rather
41511
+ * than a server error — symmetric with AWS-deployed behavior, which
41512
+ * returns `GoneException` (HTTP 410) for any connection id it can't
41513
+ * look up.
41514
+ */
41515
+ function safeDecodeURIComponent(s) {
41516
+ try {
41517
+ return decodeURIComponent(s);
41518
+ } catch {
41519
+ return null;
41520
+ }
41521
+ }
41522
+ /**
41523
+ * Read the full request body into a Buffer. Mirrors `node:http`'s
41524
+ * `IncomingMessage` consume pattern — collect chunks, resolve on `end`.
41525
+ *
41526
+ * The body is what the user's handler passed to
41527
+ * `apigatewaymanagementapi.PostToConnection({Data: <bytes>})`. AWS docs
41528
+ * say the body is raw bytes (treated as opaque by the API plane); we
41529
+ * forward the buffer through to `WebSocket.send` so binary frames work
41530
+ * end to end.
41531
+ */
41532
+ function readRequestBody(req) {
41533
+ return new Promise((resolve, reject) => {
41534
+ const chunks = [];
41535
+ req.on("data", (chunk) => {
41536
+ if (Buffer.isBuffer(chunk)) chunks.push(chunk);
41537
+ else chunks.push(Buffer.from(chunk, "utf-8"));
41538
+ });
41539
+ req.on("end", () => {
41540
+ resolve(Buffer.concat(chunks));
41541
+ });
41542
+ req.on("error", reject);
41543
+ });
41544
+ }
41545
+ /**
41546
+ * Handle a `@connections/<id>` HTTP request. Dispatches by method:
41547
+ * - `POST` → push the request body to the matching open WebSocket.
41548
+ * - `DELETE` → force-close the WebSocket (1000 normal close).
41549
+ * - `GET` → return synthetic metadata for the connection.
41550
+ * - anything else → 405.
41551
+ *
41552
+ * Returns `true` when the request was handled (caller short-circuits),
41553
+ * `false` when the URL didn't match (caller continues normal HTTP
41554
+ * route dispatch).
41555
+ *
41556
+ * AWS-correct status codes:
41557
+ * - Connection not in registry → `410 Gone` (matches AWS's
41558
+ * `GoneException` for closed connections).
41559
+ * - Send succeeded → `200 OK` (body empty).
41560
+ * - Send failed (socket not OPEN) → `410 Gone` — the connection has
41561
+ * started closing on the WebSocket side but the registry entry
41562
+ * hasn't been removed yet (the `close` event clean-up is async).
41563
+ *
41564
+ * NOTE: The body buffer can include arbitrary binary; `ws.send` handles
41565
+ * both string and Buffer inputs (the recipient receives the same bytes
41566
+ * the sender wrote).
41567
+ */
41568
+ async function handleConnectionsRequest(opts) {
41569
+ const { req, res, registry } = opts;
41570
+ const parsed = parseConnectionsPath(req.url ?? "");
41571
+ if (!parsed) {
41572
+ writeJson(res, 404, { message: "Not Found" });
41573
+ return;
41574
+ }
41575
+ const { connectionId } = parsed;
41576
+ const entry = registry.get(connectionId);
41577
+ const method = (req.method ?? "").toUpperCase();
41578
+ if (!entry) {
41579
+ writeJson(res, 410, { message: "GoneException" });
41580
+ return;
41581
+ }
41582
+ if (method === "POST") {
41583
+ let body;
41584
+ try {
41585
+ body = await readRequestBody(req);
41586
+ } catch (err) {
41587
+ writeJson(res, 500, { message: `Failed to read request body: ${err instanceof Error ? err.message : String(err)}` });
41588
+ return;
41589
+ }
41590
+ if (entry.socket.readyState !== entry.socket.OPEN) {
41591
+ writeJson(res, 410, { message: "GoneException" });
41592
+ return;
41593
+ }
41594
+ try {
41595
+ entry.socket.send(body);
41596
+ } catch (err) {
41597
+ writeJson(res, 500, { message: `Failed to deliver to socket: ${err instanceof Error ? err.message : String(err)}` });
41598
+ return;
41599
+ }
41600
+ res.writeHead(200);
41601
+ res.end();
41602
+ return;
41603
+ }
41604
+ if (method === "DELETE") {
41605
+ try {
41606
+ entry.socket.close(1e3, "DeleteConnection");
41607
+ } catch (err) {
41608
+ writeJson(res, 500, { message: `Failed to close socket: ${err instanceof Error ? err.message : String(err)}` });
41609
+ return;
41610
+ }
41611
+ res.writeHead(204);
41612
+ res.end();
41613
+ return;
41614
+ }
41615
+ if (method === "GET") {
41616
+ writeJson(res, 200, {
41617
+ ConnectedAt: new Date(entry.connectedAt).toISOString(),
41618
+ Identity: { SourceIp: "127.0.0.1" },
41619
+ LastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
41620
+ });
41621
+ return;
41622
+ }
41623
+ res.setHeader("Allow", "POST, GET, DELETE");
41624
+ writeJson(res, 405, { message: "MethodNotAllowedException" });
41625
+ }
41626
+ function writeJson(res, status, body) {
41627
+ const json = JSON.stringify(body);
41628
+ res.writeHead(status, {
41629
+ "Content-Type": "application/json",
41630
+ "Content-Length": Buffer.byteLength(json)
41631
+ });
41632
+ res.end(json);
41633
+ }
41634
+
41635
+ //#endregion
41636
+ //#region src/local/websocket-server.ts
41637
+ /**
41638
+ * Wire a WebSocket API into a long-lived `node:http`'s `upgrade`
41639
+ * pipeline. The same server already serves HTTP API v2 / REST v1 /
41640
+ * Function URL routes via the `request` listener; this module adds a
41641
+ * sibling `upgrade` listener that handles WebSocket handshakes.
41642
+ *
41643
+ * Architecture (mirrors design doc §2 / §8):
41644
+ * - One {@link WebSocketServer} per cdkd local-start-api server.
41645
+ * - `noServer: true` mode — cdkd owns the upgrade-event dispatch.
41646
+ * - Per-connection lifecycle: handshake -> $connect Lambda ->
41647
+ * (allow/deny) -> message loop -> close -> $disconnect Lambda.
41648
+ * - Outbound `@connections/<id>` POST from a handler-side AWS SDK
41649
+ * call routes to the WebSocket via the shared
41650
+ * {@link ConnectionRegistry}.
41651
+ *
41652
+ * The container pool is the SAME instance the HTTP-side server uses for
41653
+ * REST/HTTP API/Function URL routes — WebSocket dispatch is just
41654
+ * another consumer; per-Lambda concurrency caps still apply.
41655
+ */
41656
+ const DEFAULT_INVOKE_TIMEOUT_MS = 6e4;
41657
+ /**
41658
+ * Attach a WebSocket server to the parent HTTP listener. Returns an
41659
+ * {@link AttachedWebSocketServer} the CLI uses for graceful shutdown +
41660
+ * to expose the connection registry to the management-API
41661
+ * pre-pass.
41662
+ *
41663
+ * Implementation:
41664
+ * - One shared {@link ws.WebSocketServer} in `noServer` mode.
41665
+ * - One `upgrade` listener that routes by `req.url`'s pathname; an
41666
+ * unrecognized upgrade target is destroyed (RFC 6455 §4.3.2 —
41667
+ * server SHOULD respond with HTTP 404 or 426).
41668
+ * - Per-connection state held in a {@link ConnectionRegistry} the
41669
+ * `@connections` HTTP handler reads to push messages back.
41670
+ *
41671
+ * Returns synchronously — the underlying ws server is fully bound by
41672
+ * the time this function returns.
41673
+ */
41674
+ function attachWebSocketServer(opts) {
41675
+ const logger = getLogger().child("start-api/ws");
41676
+ const rieTimeoutMs = opts.rieTimeoutMs ?? DEFAULT_INVOKE_TIMEOUT_MS;
41677
+ const registry = new ConnectionRegistry();
41678
+ const wss = new WebSocketServer({ noServer: true });
41679
+ const apisByPath = /* @__PURE__ */ new Map();
41680
+ const apiPaths = [];
41681
+ for (const cfg of opts.apis) {
41682
+ apisByPath.set(cfg.apiPath, cfg);
41683
+ apiPaths.push(cfg.apiPath);
41684
+ }
41685
+ const upgradeListener = (req, socket, head) => {
41686
+ const pathOnly = (req.url ?? "/").split("?", 1)[0];
41687
+ const cfg = apisByPath.get(pathOnly);
41688
+ if (!cfg) {
41689
+ socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
41690
+ socket.destroy();
41691
+ return;
41692
+ }
41693
+ wss.handleUpgrade(req, socket, head, (ws) => {
41694
+ onConnect(ws, req, cfg).catch((err) => {
41695
+ logger.error(`WebSocket $connect dispatch failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
41696
+ try {
41697
+ ws.close(1011, "internal error");
41698
+ } catch {}
41699
+ });
41700
+ });
41701
+ };
41702
+ opts.httpServer.on("upgrade", upgradeListener);
41703
+ const MAX_PRE_VERDICT_FRAMES = 100;
41704
+ const onConnect = async (ws, req, cfg) => {
41705
+ const connectionId = randomUUID();
41706
+ const connectedAt = Date.now();
41707
+ const handshakeSnapshot = buildHandshakeSnapshot(req);
41708
+ const connectEvent = buildConnectEvent({
41709
+ connectionId,
41710
+ connectedAt,
41711
+ stage: cfg.api.stage,
41712
+ snapshot: handshakeSnapshot
41713
+ });
41714
+ const preVerdictFrames = [];
41715
+ let preVerdictOverflow = false;
41716
+ let preVerdictClosed = false;
41717
+ const preListener = (raw, isBinary) => {
41718
+ if (preVerdictClosed) return;
41719
+ if (preVerdictFrames.length >= MAX_PRE_VERDICT_FRAMES) {
41720
+ if (!preVerdictOverflow) {
41721
+ preVerdictOverflow = true;
41722
+ logger.warn(`WebSocket connection ${connectionId}: pre-verdict message buffer overflowed (>${MAX_PRE_VERDICT_FRAMES} frames). Excess frames dropped — client is sending faster than the $connect handler can resolve.`);
41723
+ }
41724
+ return;
41725
+ }
41726
+ preVerdictFrames.push({
41727
+ raw,
41728
+ isBinary
41729
+ });
41730
+ };
41731
+ ws.on("message", preListener);
41732
+ const connectRoute = cfg.api.routes.find((r) => r.routeKey === "$connect");
41733
+ if (connectRoute) {
41734
+ if (!await invokeRouteAndDecideAuth(connectRoute.targetLambdaLogicalId, connectEvent, opts.pool, rieTimeoutMs)) {
41735
+ preVerdictClosed = true;
41736
+ preVerdictFrames.length = 0;
41737
+ ws.off("message", preListener);
41738
+ try {
41739
+ ws.close(1008, "Forbidden");
41740
+ } catch {}
41741
+ logger.debug(`WebSocket $connect denied for connection ${connectionId} on ${cfg.api.declaredAt}`);
41742
+ return;
41743
+ }
41744
+ }
41745
+ preVerdictClosed = true;
41746
+ ws.off("message", preListener);
41747
+ if (ws.readyState !== ws.OPEN) {
41748
+ preVerdictFrames.length = 0;
41749
+ logger.debug(`WebSocket connection ${connectionId} closed during $connect await (readyState=${ws.readyState}) — skipping registration`);
41750
+ return;
41751
+ }
41752
+ const entry = {
41753
+ connectionId,
41754
+ socket: ws,
41755
+ connectedAt,
41756
+ apiLogicalId: cfg.api.apiLogicalId,
41757
+ stage: cfg.api.stage
41758
+ };
41759
+ registry.register(entry);
41760
+ logger.debug(`WebSocket connected: ${connectionId} (${cfg.api.declaredAt}, stage=${cfg.api.stage})`);
41761
+ ws.on("message", (raw, isBinary) => {
41762
+ const { body, isBase64Encoded } = bufferToBody(raw, isBinary);
41763
+ logger.debug(`WebSocket message received for connection ${connectionId}: ${body.slice(0, 200)}`);
41764
+ dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41765
+ logger.error(`WebSocket message dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
41766
+ try {
41767
+ ws.send(JSON.stringify({
41768
+ message: "Internal server error",
41769
+ connectionId,
41770
+ requestId: randomUUID()
41771
+ }));
41772
+ } catch {}
41773
+ });
41774
+ });
41775
+ ws.on("close", (code, reason) => {
41776
+ onDisconnect(connectionId, cfg, handshakeSnapshot, code, reason.toString("utf-8")).catch((err) => {
41777
+ logger.warn(`WebSocket $disconnect dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.message : String(err)}`);
41778
+ });
41779
+ });
41780
+ ws.on("error", (err) => {
41781
+ logger.debug(`WebSocket error for connection ${connectionId}: ${err instanceof Error ? err.message : String(err)}`);
41782
+ });
41783
+ for (const frame of preVerdictFrames) {
41784
+ const { body, isBase64Encoded } = bufferToBody(frame.raw, frame.isBinary);
41785
+ dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41786
+ logger.error(`WebSocket buffered-message dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
41787
+ });
41788
+ }
41789
+ preVerdictFrames.length = 0;
41790
+ };
41791
+ const dispatchMessage = async (connectionId, cfg, body, isBase64Encoded, snapshot) => {
41792
+ const entry = registry.get(connectionId);
41793
+ if (!entry) return;
41794
+ const routeKey = selectRouteKey(cfg.api, body);
41795
+ const route = cfg.api.routes.find((r) => r.routeKey === routeKey);
41796
+ if (!route) {
41797
+ try {
41798
+ entry.socket.send(JSON.stringify({
41799
+ message: "Internal server error",
41800
+ connectionId,
41801
+ requestId: randomUUID()
41802
+ }));
41803
+ } catch {}
41804
+ return;
41805
+ }
41806
+ const event = buildMessageEvent({
41807
+ connectionId,
41808
+ connectedAt: entry.connectedAt,
41809
+ stage: entry.stage,
41810
+ snapshot,
41811
+ routeKey,
41812
+ body,
41813
+ isBase64Encoded
41814
+ });
41815
+ await invokeRoute(route.targetLambdaLogicalId, event, opts.pool, rieTimeoutMs);
41816
+ };
41817
+ const onDisconnect = async (connectionId, cfg, snapshot, code, reason) => {
41818
+ const entry = registry.unregister(connectionId);
41819
+ if (!entry) return;
41820
+ logger.debug(`WebSocket disconnected: ${connectionId} (code=${code}, reason=${reason || "<none>"})`);
41821
+ const disconnectRoute = cfg.api.routes.find((r) => r.routeKey === "$disconnect");
41822
+ if (!disconnectRoute) return;
41823
+ const event = buildDisconnectEvent({
41824
+ connectionId,
41825
+ connectedAt: entry.connectedAt,
41826
+ stage: entry.stage,
41827
+ snapshot,
41828
+ disconnectStatusCode: code,
41829
+ disconnectReason: reason
41830
+ });
41831
+ await invokeRoute(disconnectRoute.targetLambdaLogicalId, event, opts.pool, rieTimeoutMs);
41832
+ };
41833
+ let closed = false;
41834
+ return {
41835
+ registry,
41836
+ apiPaths,
41837
+ close: async () => {
41838
+ if (closed) return;
41839
+ closed = true;
41840
+ opts.httpServer.off("upgrade", upgradeListener);
41841
+ const closes = Array.from(wss.clients).map((ws) => new Promise((resolve) => {
41842
+ const onClose = () => resolve();
41843
+ ws.once("close", onClose);
41844
+ try {
41845
+ ws.close(1001, "going away");
41846
+ } catch {
41847
+ resolve();
41848
+ }
41849
+ setTimeout(() => {
41850
+ ws.off("close", onClose);
41851
+ resolve();
41852
+ }, 5e3).unref();
41853
+ }));
41854
+ await Promise.all(closes);
41855
+ await new Promise((resolve) => {
41856
+ wss.close(() => resolve());
41857
+ });
41858
+ }
41859
+ };
41860
+ }
41861
+ /**
41862
+ * Pre-pass for the HTTP `request` listener: intercept `POST/GET/DELETE
41863
+ * /@connections/<id>` calls and route them to the connection registry.
41864
+ *
41865
+ * Returns `true` when the request was handled (caller short-circuits
41866
+ * the normal HTTP dispatch path), `false` when the URL didn't match.
41867
+ *
41868
+ * The CLI installs this BEFORE the existing http-server pipeline so a
41869
+ * Lambda inside a container can call
41870
+ * `apigatewaymanagementapi:PostToConnection` and have cdkd deliver the
41871
+ * message back to the open WebSocket without the request hitting the
41872
+ * route table.
41873
+ */
41874
+ async function handleManagementRequest(req, res, registry) {
41875
+ if (parseConnectionsPath(req.url ?? "") === null) return false;
41876
+ await handleConnectionsRequest({
41877
+ req,
41878
+ res,
41879
+ registry
41880
+ });
41881
+ return true;
41882
+ }
41883
+ /**
41884
+ * Select the route the client message dispatches to.
41885
+ *
41886
+ * Algorithm (matches AWS docs §"Selection expressions"):
41887
+ * 1. Try to parse the body as JSON. Non-JSON → `$default`.
41888
+ * 2. Walk the selection-expression's JSON-path tokens against the
41889
+ * parsed body. Missing intermediate keys → `$default`.
41890
+ * 3. The final value's `String()` representation is the route key.
41891
+ * 4. When that key has no matching route, fall back to `$default`.
41892
+ *
41893
+ * v1's selection-expression grammar is `$request.body.<key>` (with
41894
+ * optional nested dot access). Other shapes were rejected upstream at
41895
+ * discovery time.
41896
+ */
41897
+ function selectRouteKey(api, body) {
41898
+ let parsed;
41899
+ try {
41900
+ parsed = JSON.parse(body);
41901
+ } catch {
41902
+ return "$default";
41903
+ }
41904
+ const tokens = parseSelectionExpressionPath(api.routeSelectionExpression);
41905
+ let cursor = parsed;
41906
+ for (const token of tokens) {
41907
+ if (cursor === null || typeof cursor !== "object") return "$default";
41908
+ cursor = cursor[token];
41909
+ if (cursor === void 0) return "$default";
41910
+ }
41911
+ const candidate = String(cursor);
41912
+ if (api.routes.some((r) => r.routeKey === candidate)) return candidate;
41913
+ return "$default";
41914
+ }
41915
+ /**
41916
+ * Invoke a route's Lambda for side effects only (MESSAGE / DISCONNECT
41917
+ * paths). The Lambda's response is intentionally discarded — AWS-deployed
41918
+ * WebSocket APIs do the same; handlers reply via `PostToConnection`.
41919
+ */
41920
+ async function invokeRoute(lambdaLogicalId, event, pool, rieTimeoutMs) {
41921
+ const handle = await pool.acquire(lambdaLogicalId);
41922
+ try {
41923
+ await invokeRie(handle.containerHost, handle.hostPort, event, rieTimeoutMs);
41924
+ } finally {
41925
+ pool.release(handle);
41926
+ }
41927
+ }
41928
+ /**
41929
+ * Invoke the `$connect` Lambda and decide whether to accept the
41930
+ * connection. AWS-deployed behavior: handler returns `{statusCode:
41931
+ * 200}` (or any 2xx) → allow; anything else (non-2xx, error envelope,
41932
+ * throw, timeout) → deny.
41933
+ */
41934
+ async function invokeRouteAndDecideAuth(lambdaLogicalId, event, pool, rieTimeoutMs) {
41935
+ let result;
41936
+ try {
41937
+ const handle = await pool.acquire(lambdaLogicalId);
41938
+ try {
41939
+ result = await invokeRie(handle.containerHost, handle.hostPort, event, rieTimeoutMs);
41940
+ } finally {
41941
+ pool.release(handle);
41942
+ }
41943
+ } catch {
41944
+ return false;
41945
+ }
41946
+ if (result.payload && typeof result.payload === "object") {
41947
+ const obj = result.payload;
41948
+ if (typeof obj["errorMessage"] === "string" && typeof obj["statusCode"] !== "number") return false;
41949
+ const status = obj["statusCode"];
41950
+ if (typeof status === "number") return status >= 200 && status < 300;
41951
+ }
41952
+ return true;
41953
+ }
41954
+ /**
41955
+ * Snapshot the upgrade-request data the event-builders need. We capture
41956
+ * this ONCE at `$connect` and reuse it for every event on the same
41957
+ * connection — `requestContext.identity.sourceIp` etc. must stay
41958
+ * consistent across CONNECT / MESSAGE / DISCONNECT (matches AWS).
41959
+ */
41960
+ function buildHandshakeSnapshot(req) {
41961
+ const headers = {};
41962
+ for (const [name, value] of Object.entries(req.headers)) {
41963
+ if (value === void 0) continue;
41964
+ headers[name] = Array.isArray(value) ? [...value] : [value];
41965
+ }
41966
+ const url = req.url ?? "/";
41967
+ const queryIdx = url.indexOf("?");
41968
+ const rawQueryString = queryIdx >= 0 ? url.slice(queryIdx + 1) : "";
41969
+ const { single, multi } = parseQueryString(rawQueryString);
41970
+ const userAgent = typeof req.headers["user-agent"] === "string" ? req.headers["user-agent"] : void 0;
41971
+ const sourceIp = req.socket.remoteAddress;
41972
+ return {
41973
+ headers,
41974
+ rawQueryString,
41975
+ ...Object.keys(single).length > 0 && { queryStringParameters: single },
41976
+ ...Object.keys(multi).length > 0 && { multiValueQueryStringParameters: multi },
41977
+ ...sourceIp !== void 0 && { sourceIp },
41978
+ ...userAgent !== void 0 && { userAgent }
41979
+ };
41980
+ }
41981
+ /**
41982
+ * Parse a raw query string into single-value (last-wins per AWS) and
41983
+ * multi-value maps. Mirrors `route-discovery.ts:parseQueryStringSingular`'s
41984
+ * convention — duplicated locally rather than reaching across modules
41985
+ * because the WebSocket path is a thin slice that does not need the
41986
+ * full HTTP-API parser.
41987
+ */
41988
+ function parseQueryString(qs) {
41989
+ const single = {};
41990
+ const multi = {};
41991
+ if (qs.length === 0) return {
41992
+ single,
41993
+ multi
41994
+ };
41995
+ for (const pair of qs.split("&")) {
41996
+ if (pair.length === 0) continue;
41997
+ const eq = pair.indexOf("=");
41998
+ const rawKey = eq >= 0 ? pair.slice(0, eq) : pair;
41999
+ const rawVal = eq >= 0 ? pair.slice(eq + 1) : "";
42000
+ const key = safeDecode$1(rawKey);
42001
+ const val = safeDecode$1(rawVal);
42002
+ if (key === null) continue;
42003
+ single[key] = val ?? "";
42004
+ (multi[key] ??= []).push(val ?? "");
42005
+ }
42006
+ return {
42007
+ single,
42008
+ multi
42009
+ };
42010
+ }
42011
+ function safeDecode$1(s) {
42012
+ try {
42013
+ return decodeURIComponent(s.replace(/\+/g, " "));
42014
+ } catch {
42015
+ return null;
42016
+ }
42017
+ }
42018
+ /**
42019
+ * Convert a ws-emitted message buffer into the AWS-canonical event
42020
+ * body + `isBase64Encoded` discriminator. Text frames (opcode 0x1) pass
42021
+ * through as UTF-8 with `isBase64Encoded: false`; binary frames
42022
+ * (opcode 0x2) are base64-encoded with `isBase64Encoded: true`. Matches
42023
+ * AWS-deployed WebSocket API event shape exactly — handlers decode via
42024
+ * `Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')`.
42025
+ *
42026
+ * Closes the data-integrity bug where every byte > 0x7F on a binary
42027
+ * frame was silently corrupted by handlers that trusted the previously
42028
+ * hardcoded `isBase64Encoded: false` flag and UTF-8-decoded the
42029
+ * base64-encoded body.
42030
+ */
42031
+ function bufferToBody(raw, isBinary) {
42032
+ const buf = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
42033
+ if (isBinary) return {
42034
+ body: buf.toString("base64"),
42035
+ isBase64Encoded: true
42036
+ };
42037
+ return {
42038
+ body: buf.toString("utf-8"),
42039
+ isBase64Encoded: false
42040
+ };
42041
+ }
42042
+
41203
42043
  //#endregion
41204
42044
  //#region src/local/vtl-engine.ts
41205
42045
  /** Error thrown when a template references an unsupported VTL feature. */
@@ -42737,7 +43577,8 @@ function createContainerPool(specs, options) {
42737
43577
  host: spec.containerHost,
42738
43578
  name,
42739
43579
  ...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
42740
- ...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
43580
+ ...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs },
43581
+ ...spec.extraHosts !== void 0 && { extraHosts: spec.extraHosts }
42741
43582
  });
42742
43583
  } else containerId = await runDetached({
42743
43584
  image: spec.image,
@@ -42751,7 +43592,8 @@ function createContainerPool(specs, options) {
42751
43592
  ...spec.entryPoint !== void 0 && { entryPoint: spec.entryPoint },
42752
43593
  ...spec.workingDir !== void 0 && { workingDir: spec.workingDir },
42753
43594
  ...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
42754
- ...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
43595
+ ...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs },
43596
+ ...spec.extraHosts !== void 0 && { extraHosts: spec.extraHosts }
42755
43597
  });
42756
43598
  const stopLogStream = streamingEnabled ? streamLogs(containerId) : () => void 0;
42757
43599
  try {
@@ -45607,6 +46449,13 @@ async function startApiServer(opts) {
45607
46449
  */
45608
46450
  async function handleRequest(req, res, state, opts) {
45609
46451
  const logger = getLogger().child("start-api");
46452
+ if (opts.preDispatch) try {
46453
+ if (await opts.preDispatch(req, res)) return;
46454
+ } catch (err) {
46455
+ logger.error(`preDispatch hook threw: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
46456
+ if (!res.headersSent) writeError(res, 502);
46457
+ return;
46458
+ }
45610
46459
  const bodyBuf = await readBody(req);
45611
46460
  const rawUrl = req.url ?? "/";
45612
46461
  const method = (req.method ?? "GET").toUpperCase();
@@ -46992,13 +47841,18 @@ async function localStartApiCommand(target, options) {
46992
47841
  output: options.output,
46993
47842
  ...options.region && { region: options.region },
46994
47843
  ...options.profile && { profile: options.profile },
46995
- ...Object.keys(context).length > 0 && { context }
47844
+ ...Object.keys(context).length > 0 && { context },
47845
+ ...options.stateBucket && { stateBucket: options.stateBucket },
47846
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
46996
47847
  };
46997
47848
  const { stacks } = await synthesizer.synthesize(synthOpts);
46998
47849
  const targetStacks = pickTargetStacks(stacks, options.stack);
46999
47850
  if (targetStacks.length === 0) throw new Error("No stacks matched. Pass --stack <name> or run from a single-stack app.");
47000
47851
  const routes = discoverRoutes(targetStacks);
47001
- if (routes.length === 0) throw new Error("No supported API routes were discovered. cdkd local start-api supports AWS::ApiGateway::* (REST v1), AWS::ApiGatewayV2::* (HTTP), and AWS::Lambda::Url (Function URL) with AWS_PROXY integrations only.");
47852
+ const wsDiscovery = discoverWebSocketApis(targetStacks);
47853
+ if (wsDiscovery.errors.length > 0) for (const e of wsDiscovery.errors) logger.warn(`WebSocket discovery: ${e}`);
47854
+ const webSocketApis = wsDiscovery.apis;
47855
+ if (routes.length === 0 && webSocketApis.length === 0) throw new Error("No supported API routes were discovered. cdkd local start-api supports AWS::ApiGateway::* (REST v1), AWS::ApiGatewayV2::* (HTTP + WebSocket), and AWS::Lambda::Url (Function URL) with AWS_PROXY integrations only.");
47002
47856
  const stageMap = /* @__PURE__ */ new Map();
47003
47857
  for (const stack of targetStacks) {
47004
47858
  const m = buildStageMap(stack.template, options.stage);
@@ -47029,7 +47883,7 @@ async function localStartApiCommand(target, options) {
47029
47883
  for (const [k, v] of m) corsConfigByApiId.set(k, v);
47030
47884
  }
47031
47885
  const stateByStack = options.fromState ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
47032
- const lambdaIds = uniqueLambdaIds(routes, routesWithAuth);
47886
+ const lambdaIds = uniqueLambdaIds(routes, routesWithAuth, webSocketApis);
47033
47887
  const specs = /* @__PURE__ */ new Map();
47034
47888
  for (let i = 0; i < lambdaIds.length; i++) {
47035
47889
  const logicalId = lambdaIds[i];
@@ -47056,6 +47910,7 @@ async function localStartApiCommand(target, options) {
47056
47910
  routes: routesWithAuth,
47057
47911
  specs,
47058
47912
  corsConfigByApiId,
47913
+ webSocketApis,
47059
47914
  stacks: targetStacks
47060
47915
  };
47061
47916
  };
@@ -47149,12 +48004,88 @@ async function localStartApiCommand(target, options) {
47149
48004
  });
47150
48005
  if (basePort !== 0) nextPort += 1;
47151
48006
  }
48007
+ const wsServers = [];
48008
+ const initialWsApis = initialMaterial.webSocketApis ?? [];
48009
+ warnUnsupportedWebSocketApis(initialWsApis, logger);
48010
+ for (const api of initialWsApis) {
48011
+ if (api.unsupported) continue;
48012
+ const wsLambdaIds = new Set(api.routes.map((r) => r.targetLambdaLogicalId));
48013
+ const wsSpecs = /* @__PURE__ */ new Map();
48014
+ for (const id of wsLambdaIds) {
48015
+ const spec = initialMaterial.specs.get(id);
48016
+ if (spec) wsSpecs.set(id, spec);
48017
+ }
48018
+ if (wsSpecs.size === 0) {
48019
+ logger.warn(`WebSocket API ${api.declaredAt}: no resolvable Lambda backing routes; skipping.`);
48020
+ continue;
48021
+ }
48022
+ const wsPool = buildPool(wsSpecs);
48023
+ const wsState = {
48024
+ routes: [],
48025
+ pool: wsPool,
48026
+ corsConfigByApiId: /* @__PURE__ */ new Map()
48027
+ };
48028
+ const wsApiPath = `/${api.stage}`;
48029
+ let registryRef;
48030
+ const started = await startApiServer({
48031
+ state: wsState,
48032
+ rieTimeoutMs,
48033
+ host: options.host,
48034
+ port: basePort === 0 ? 0 : nextPort,
48035
+ authorizerCache,
48036
+ jwksCache,
48037
+ jwksWarnedUrls,
48038
+ sigV4WarnedForeignIds,
48039
+ sigV4AllowUnverified: options.allowUnverifiedSigv4 === true,
48040
+ preDispatch: async (req, res) => {
48041
+ if (!registryRef) return false;
48042
+ return handleManagementRequest(req, res, registryRef.registry);
48043
+ }
48044
+ });
48045
+ const attached = attachWebSocketServer({
48046
+ httpServer: started.server,
48047
+ pool: wsPool,
48048
+ rieTimeoutMs,
48049
+ apis: [{
48050
+ api,
48051
+ apiPath: wsApiPath
48052
+ }]
48053
+ });
48054
+ registryRef = attached;
48055
+ const mgmtEndpoint = `http://host.docker.internal:${started.port}/${api.stage}`;
48056
+ const hostGatewayMapping = [{
48057
+ host: "host.docker.internal",
48058
+ ip: "host-gateway"
48059
+ }];
48060
+ for (const id of wsLambdaIds) {
48061
+ const spec = initialMaterial.specs.get(id);
48062
+ if (!spec) continue;
48063
+ spec.env["AWS_ENDPOINT_URL_APIGATEWAYMANAGEMENTAPI"] = mgmtEndpoint;
48064
+ if (!spec.env["AWS_ACCESS_KEY_ID"]) {
48065
+ spec.env["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE";
48066
+ spec.env["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
48067
+ }
48068
+ if (!spec.env["AWS_REGION"]) spec.env["AWS_REGION"] = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? "us-east-1";
48069
+ spec.extraHosts = [...spec.extraHosts ?? [], ...hostGatewayMapping];
48070
+ }
48071
+ wsServers.push({
48072
+ api,
48073
+ server: started,
48074
+ attached,
48075
+ apiPath: wsApiPath
48076
+ });
48077
+ if (basePort !== 0) nextPort += 1;
48078
+ }
47152
48079
  printPerServerRouteTables(servers);
47153
48080
  const allRoutes = servers.flatMap((s) => s.group.routes.map((r) => r.route));
47154
48081
  warnUnsupportedRoutes(allRoutes, logger);
47155
48082
  warnSsrfRiskyIntegrations(allRoutes, logger);
47156
48083
  logger.info(`Per-Lambda concurrency: ${perLambdaConcurrency} (override with --per-lambda-concurrency)`);
47157
48084
  for (const { group, server } of servers) process.stdout.write(`Server listening on ${server.scheme}://${server.host}:${server.port} (${group.displayName})\n`);
48085
+ for (const ws of wsServers) {
48086
+ const scheme = ws.server.scheme === "https" ? "wss" : "ws";
48087
+ process.stdout.write(`Server listening on ${scheme}://${ws.server.host}:${ws.server.port}${ws.apiPath} (${ws.api.apiLogicalId} (WebSocket API))\n`);
48088
+ }
47158
48089
  process.stdout.write("^C to stop and clean up containers.\n");
47159
48090
  let watcher;
47160
48091
  let reloadChain = Promise.resolve();
@@ -47188,6 +48119,13 @@ async function localStartApiCommand(target, options) {
47188
48119
  } catch (err) {
47189
48120
  logger.warn(`watcher.close() failed: ${err instanceof Error ? err.message : String(err)}`);
47190
48121
  }
48122
+ await Promise.allSettled(wsServers.map(async (ws) => {
48123
+ try {
48124
+ await ws.attached.close();
48125
+ } catch (err) {
48126
+ logger.warn(`WebSocket close() failed for ${ws.api.apiLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
48127
+ }
48128
+ }));
47191
48129
  await Promise.allSettled(servers.map(async ({ server, group }) => {
47192
48130
  try {
47193
48131
  await server.close();
@@ -47195,6 +48133,13 @@ async function localStartApiCommand(target, options) {
47195
48133
  logger.warn(`server.close() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`);
47196
48134
  }
47197
48135
  }));
48136
+ await Promise.allSettled(wsServers.map(async (ws) => {
48137
+ try {
48138
+ await ws.server.close();
48139
+ } catch (err) {
48140
+ logger.warn(`WebSocket server.close() failed for ${ws.api.apiLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
48141
+ }
48142
+ }));
47198
48143
  await Promise.allSettled(servers.map(async ({ server, group }) => {
47199
48144
  try {
47200
48145
  await server.getServerState().pool.dispose();
@@ -47202,6 +48147,13 @@ async function localStartApiCommand(target, options) {
47202
48147
  logger.warn(`pool.dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`);
47203
48148
  }
47204
48149
  }));
48150
+ await Promise.allSettled(wsServers.map(async (ws) => {
48151
+ try {
48152
+ await ws.server.getServerState().pool.dispose();
48153
+ } catch (err) {
48154
+ logger.warn(`WebSocket pool.dispose() failed for ${ws.api.apiLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
48155
+ }
48156
+ }));
47205
48157
  for (const dir of inlineTmpDirs) try {
47206
48158
  rmSync(dir, {
47207
48159
  recursive: true,
@@ -47268,7 +48220,7 @@ function pickTargetStacks(stacks, pattern) {
47268
48220
  * list, then any newly-introduced authorizer Lambdas, which keeps the
47269
48221
  * route-table output deterministic.
47270
48222
  */
47271
- function uniqueLambdaIds(routes, routesWithAuth) {
48223
+ function uniqueLambdaIds(routes, routesWithAuth, webSocketApis = []) {
47272
48224
  const seen = /* @__PURE__ */ new Set();
47273
48225
  const out = [];
47274
48226
  for (const r of routes) {
@@ -47290,6 +48242,10 @@ function uniqueLambdaIds(routes, routesWithAuth) {
47290
48242
  }
47291
48243
  }
47292
48244
  }
48245
+ for (const api of webSocketApis) for (const r of api.routes) if (!seen.has(r.targetLambdaLogicalId)) {
48246
+ seen.add(r.targetLambdaLogicalId);
48247
+ out.push(r.targetLambdaLogicalId);
48248
+ }
47293
48249
  return out;
47294
48250
  }
47295
48251
  /**
@@ -47853,6 +48809,24 @@ function warnUnsupportedRoutes(routes, logger) {
47853
48809
  return unsupported.length;
47854
48810
  }
47855
48811
  /**
48812
+ * Surface every WebSocket API tagged as unsupported at discovery as a
48813
+ * startup warn. The boot loop above skips attaching the server for
48814
+ * these APIs, so no upgrade requests are ever accepted on them —
48815
+ * mirrors `warnUnsupportedRoutes`'s shape but for the WebSocket axis.
48816
+ * Typical trigger: a Route declaring `AuthorizationType !== 'NONE'` on
48817
+ * `$connect` (cdkd v1 does not emulate WebSocket authorizers; closing
48818
+ * this gap structurally rather than silently admitting
48819
+ * unauthenticated clients matches the security-by-default precedent
48820
+ * PR #514 set for HTTP API v2 service integrations).
48821
+ */
48822
+ function warnUnsupportedWebSocketApis(apis, logger) {
48823
+ const unsupported = apis.filter((api) => api.unsupported);
48824
+ if (unsupported.length === 0) return 0;
48825
+ logger.warn(`${unsupported.length} WebSocket API(s) will NOT accept upgrade requests (boot continued):`);
48826
+ for (const api of unsupported) logger.warn(` - ${api.declaredAt}: ${api.unsupported.reason}`);
48827
+ return unsupported.length;
48828
+ }
48829
+ /**
47856
48830
  * Surface a one-line warn per HTTP / HTTP_PROXY integration whose
47857
48831
  * `Integration.Uri` points at a well-known internal address space
47858
48832
  * (AWS IMDS, loopback, link-local, RFC1918). PR #505 / issue #457
@@ -48920,7 +49894,9 @@ async function localRunTaskCommand(target, options) {
48920
49894
  output: options.output,
48921
49895
  ...options.region && { region: options.region },
48922
49896
  ...options.profile && { profile: options.profile },
48923
- ...Object.keys(context).length > 0 && { context }
49897
+ ...Object.keys(context).length > 0 && { context },
49898
+ ...options.stateBucket && { stateBucket: options.stateBucket },
49899
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
48924
49900
  };
48925
49901
  const { stacks } = await synthesizer.synthesize(synthOpts);
48926
49902
  const imageContext = await buildEcsImageResolutionContext$1(target, stacks, options);
@@ -49601,7 +50577,9 @@ async function localStartServiceCommand(target, options) {
49601
50577
  output: options.output,
49602
50578
  ...options.region && { region: options.region },
49603
50579
  ...options.profile && { profile: options.profile },
49604
- ...Object.keys(context).length > 0 && { context }
50580
+ ...Object.keys(context).length > 0 && { context },
50581
+ ...options.stateBucket && { stateBucket: options.stateBucket },
50582
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
49605
50583
  };
49606
50584
  const { stacks } = await synthesizer.synthesize(synthOpts);
49607
50585
  const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
@@ -49922,7 +50900,9 @@ async function localInvokeCommand(target, options) {
49922
50900
  output: options.output,
49923
50901
  ...options.region && { region: options.region },
49924
50902
  ...options.profile && { profile: options.profile },
49925
- ...Object.keys(context).length > 0 && { context }
50903
+ ...Object.keys(context).length > 0 && { context },
50904
+ ...options.stateBucket && { stateBucket: options.stateBucket },
50905
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
49926
50906
  };
49927
50907
  const { stacks } = await synthesizer.synthesize(synthOpts);
49928
50908
  const lambda = resolveLambdaTarget(target, stacks);
@@ -50830,7 +51810,9 @@ async function exportCommand(stackArg, options) {
50830
51810
  const result = await synthesizer.synthesize({
50831
51811
  app: appCmd,
50832
51812
  output: options.output || "cdk.out",
50833
- ...Object.keys(context).length > 0 && { context }
51813
+ ...Object.keys(context).length > 0 && { context },
51814
+ stateBucket,
51815
+ ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
50834
51816
  });
50835
51817
  let stackInfo;
50836
51818
  if (stackArg) {
@@ -52999,7 +53981,7 @@ function reorderArgs(argv) {
52999
53981
  */
53000
53982
  async function main() {
53001
53983
  const program = new Command();
53002
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.135.1");
53984
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.137.0");
53003
53985
  program.addCommand(createBootstrapCommand());
53004
53986
  program.addCommand(createSynthCommand());
53005
53987
  program.addCommand(createListCommand());