@go-to-k/cdkd 0.128.0 → 0.130.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-CuHRHcyW.js";
3
- import { A as AssetPublisher, B as getLegacyStateBucketName, C as applyRoleArnIfSet, Ct as generateResourceNameWithFallback, D as LockManager, E as TemplateParser, F as getDockerCmd, G as resolveStateBucketWithDefaultAndSource, H as resolveCaptureObservedState, I as runDockerForeground, K as warnDeprecatedNoPrefixCliFlag, L as runDockerStreaming, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as formatDockerLoginError, R as Synthesizer, S as IntrinsicFunctionResolver, St as generateResourceName, T as DagBuilder, Tt as withStackName, U as resolveSkipPrefix, V as resolveApp, W as resolveStateBucketWithDefault, Y as resolveBucketRegion, Z as CdkdError, _ as normalizeAwsTagsToCfn, a as withRetry, at as ResourceUpdateNotSupportedError, b as CloudControlProvider, bt as PATTERN_B_NAME_PROPERTIES, c as cyan, ct as StackTerminationProtectionError, d as red, et as LocalInvokeBuildError, f as yellow, g as matchesCdkPath, gt as getLogger, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceTimeoutError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, mt as withErrorHandling, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as PartialFailureError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as RouteDiscoveryError, p as IAMRoleProvider, pt as normalizeAwsError, q as AssemblyReader, r as DeployEngine, rt as ProvisioningError, s as bold, st as StackHasActiveImportsError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as green, v as resolveExplicitPhysicalId, vt as runStackBuffered, w as DiffCalculator, wt as withSkipPrefix, x as assertRegionMatch, xt as PATTERN_B_RESOURCE_TYPES, y as ProviderRegistry, yt as getLiveRenderer, z as getDefaultStateBucketName } from "./deploy-engine-DWpeb9wT.js";
3
+ import { A as AssetPublisher, B as getLegacyStateBucketName, C as applyRoleArnIfSet, Ct as generateResourceNameWithFallback, D as LockManager, E as TemplateParser, F as getDockerCmd, G as resolveStateBucketWithDefaultAndSource, H as resolveCaptureObservedState, I as runDockerForeground, K as warnDeprecatedNoPrefixCliFlag, L as runDockerStreaming, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as formatDockerLoginError, R as Synthesizer, S as IntrinsicFunctionResolver, St as generateResourceName, T as DagBuilder, Tt as withStackName, U as resolveSkipPrefix, V as resolveApp, W as resolveStateBucketWithDefault, Y as resolveBucketRegion, Z as CdkdError, _ as normalizeAwsTagsToCfn, a as withRetry, at as ResourceUpdateNotSupportedError, b as CloudControlProvider, bt as PATTERN_B_NAME_PROPERTIES, c as cyan, ct as StackTerminationProtectionError, d as red, et as LocalInvokeBuildError, f as yellow, g as matchesCdkPath, gt as getLogger, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceTimeoutError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, mt as withErrorHandling, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as PartialFailureError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as RouteDiscoveryError, p as IAMRoleProvider, pt as normalizeAwsError, q as AssemblyReader, r as DeployEngine, rt as ProvisioningError, s as bold, st as StackHasActiveImportsError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as green, v as resolveExplicitPhysicalId, vt as runStackBuffered, w as DiffCalculator, wt as withSkipPrefix, x as assertRegionMatch, xt as PATTERN_B_RESOURCE_TYPES, y as ProviderRegistry, yt as getLiveRenderer, z as getDefaultStateBucketName } from "./deploy-engine-B-w4C_7O.js";
4
4
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
6
6
  import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
@@ -39464,13 +39464,16 @@ async function invokeRie(host, port, event, timeoutMs) {
39464
39464
  * something we should retry. Abort errors propagate immediately so the
39465
39465
  * outer timeout still wins.
39466
39466
  */
39467
- async function fetchWithStartupRetry(url, body, signal) {
39467
+ async function fetchWithStartupRetry(url, body, signal, extraHeaders) {
39468
39468
  const maxAttempts = 3;
39469
39469
  let lastError;
39470
39470
  for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
39471
39471
  return await fetch(url, {
39472
39472
  method: "POST",
39473
- headers: { "Content-Type": "application/json" },
39473
+ headers: {
39474
+ "Content-Type": "application/json",
39475
+ ...extraHeaders
39476
+ },
39474
39477
  body,
39475
39478
  signal
39476
39479
  });
@@ -39482,6 +39485,157 @@ async function fetchWithStartupRetry(url, body, signal) {
39482
39485
  }
39483
39486
  throw lastError;
39484
39487
  }
39488
+ /**
39489
+ * The 8-NULL-byte separator AWS Lambda RIE writes between the JSON
39490
+ * metadata prelude and the streaming body chunks. Empirically verified
39491
+ * — see `StreamingPrelude` docstring above.
39492
+ */
39493
+ const STREAM_PRELUDE_SEPARATOR = Buffer.from([
39494
+ 0,
39495
+ 0,
39496
+ 0,
39497
+ 0,
39498
+ 0,
39499
+ 0,
39500
+ 0,
39501
+ 0
39502
+ ]);
39503
+ /**
39504
+ * Maximum bytes we'll buffer searching for the prelude separator before
39505
+ * giving up. 1 MiB is far past anything Lambda's streamifyResponse would
39506
+ * emit as metadata (typical preludes are <500 bytes) — a runaway here
39507
+ * indicates the handler didn't call `HttpResponseStream.from` at all, in
39508
+ * which case we want to fail fast rather than buffer the whole body.
39509
+ */
39510
+ const STREAM_PRELUDE_MAX_BYTES = 1024 * 1024;
39511
+ /**
39512
+ * POST the event payload to RIE with the `streaming` response-mode
39513
+ * header, parse the JSON prelude out of the response bytes, and return
39514
+ * a Readable carrying the post-separator body chunks.
39515
+ *
39516
+ * Why a separate function from `invokeRie`: the prelude/separator/body
39517
+ * framing is incompatible with the buffered-response `text()` consumer.
39518
+ * Buffered routes still use `invokeRie`; only Function URLs with
39519
+ * `InvokeMode: RESPONSE_STREAM` use this path.
39520
+ *
39521
+ * The `Lambda-Runtime-Function-Response-Mode: streaming` request header
39522
+ * tells RIE we want the streaming protocol. (RIE happens to emit the
39523
+ * same protocol for `streamifyResponse`-wrapped handlers regardless of
39524
+ * the header, but setting it makes the contract explicit and survives
39525
+ * future RIE behavior changes.)
39526
+ *
39527
+ * `timeoutMs` bounds the total wall time including the prelude wait —
39528
+ * the handler can stream for the full Lambda timeout, so callers should
39529
+ * pass a generous value (typically the function's configured Timeout * 2
39530
+ * with a 30s floor, matching `invokeRie`'s convention).
39531
+ */
39532
+ async function invokeRieStreaming(host, port, event, timeoutMs) {
39533
+ const url = `http://${host}:${port}${INVOKE_PATH}`;
39534
+ const body = JSON.stringify(event ?? {});
39535
+ const controller = new AbortController();
39536
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
39537
+ let response;
39538
+ try {
39539
+ response = await fetchWithStartupRetry(url, body, controller.signal, { "Lambda-Runtime-Function-Response-Mode": "streaming" });
39540
+ } catch (err) {
39541
+ clearTimeout(timer);
39542
+ if (err.name === "AbortError") throw new Error(`RIE streaming invoke at ${url} timed out after ${timeoutMs}ms. The handler may be hung; check container logs.`);
39543
+ throw err;
39544
+ }
39545
+ if (!response.body) {
39546
+ clearTimeout(timer);
39547
+ throw new Error(`RIE streaming invoke at ${url} returned no response body.`);
39548
+ }
39549
+ const reader = response.body.getReader();
39550
+ let preludeBytes = Buffer.alloc(0);
39551
+ let bodyTail;
39552
+ let separatorIdx = -1;
39553
+ while (separatorIdx < 0) {
39554
+ const { value, done } = await reader.read();
39555
+ if (done) break;
39556
+ const chunk = Buffer.from(value);
39557
+ preludeBytes = Buffer.concat([preludeBytes, chunk]);
39558
+ separatorIdx = preludeBytes.indexOf(STREAM_PRELUDE_SEPARATOR);
39559
+ if (separatorIdx >= 0) {
39560
+ bodyTail = preludeBytes.subarray(separatorIdx + STREAM_PRELUDE_SEPARATOR.length);
39561
+ preludeBytes = preludeBytes.subarray(0, separatorIdx);
39562
+ break;
39563
+ }
39564
+ if (preludeBytes.length > STREAM_PRELUDE_MAX_BYTES) {
39565
+ clearTimeout(timer);
39566
+ reader.cancel().catch(() => void 0);
39567
+ throw new Error(`RIE streaming response did not emit the prelude/body separator within ${STREAM_PRELUDE_MAX_BYTES} bytes. The handler likely did not call awslambda.HttpResponseStream.from(stream, metadata).`);
39568
+ }
39569
+ }
39570
+ if (separatorIdx < 0) {
39571
+ clearTimeout(timer);
39572
+ throw new Error(`RIE streaming response ended before the prelude/body separator (got ${preludeBytes.length} bytes). The handler likely threw before streaming the prelude — check container logs.`);
39573
+ }
39574
+ let prelude;
39575
+ try {
39576
+ prelude = parseStreamingPrelude(preludeBytes.toString("utf8"));
39577
+ } catch (err) {
39578
+ clearTimeout(timer);
39579
+ reader.cancel().catch(() => void 0);
39580
+ throw new Error(`RIE streaming response prelude is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
39581
+ }
39582
+ const stream = new Readable({ read() {} });
39583
+ (async () => {
39584
+ try {
39585
+ if (bodyTail && bodyTail.length > 0) stream.push(bodyTail);
39586
+ while (true) {
39587
+ const { value, done } = await reader.read();
39588
+ if (done) break;
39589
+ stream.push(Buffer.from(value));
39590
+ }
39591
+ stream.push(null);
39592
+ } catch (err) {
39593
+ stream.destroy(err instanceof Error ? err : new Error(String(err)));
39594
+ } finally {
39595
+ clearTimeout(timer);
39596
+ }
39597
+ })();
39598
+ return {
39599
+ prelude,
39600
+ body: stream
39601
+ };
39602
+ }
39603
+ /**
39604
+ * Parse a streaming prelude payload (JSON text). Normalizes the shape
39605
+ * the http-server consumes: `statusCode` is coerced to a number (RIE
39606
+ * sometimes emits it as a string), `headers` is always an object (the
39607
+ * handler may omit it), `cookies` is preserved only when an array.
39608
+ *
39609
+ * Exported for unit tests. Throws on invalid JSON or a non-numeric
39610
+ * statusCode (cdkd cannot map that to HTTP).
39611
+ */
39612
+ function parseStreamingPrelude(text) {
39613
+ const trimmed = text.trim();
39614
+ if (trimmed.length === 0) throw new Error("empty prelude");
39615
+ const raw = JSON.parse(trimmed);
39616
+ if (!raw || typeof raw !== "object") throw new Error("prelude is not a JSON object");
39617
+ const obj = raw;
39618
+ const statusRaw = obj["statusCode"];
39619
+ let statusCode;
39620
+ if (typeof statusRaw === "number" && Number.isFinite(statusRaw)) statusCode = Math.trunc(statusRaw);
39621
+ else if (typeof statusRaw === "string" && /^[0-9]+$/.test(statusRaw)) statusCode = Number.parseInt(statusRaw, 10);
39622
+ else throw new Error(`statusCode must be a number (got ${typeof statusRaw})`);
39623
+ const headers = {};
39624
+ const headersRaw = obj["headers"];
39625
+ if (headersRaw && typeof headersRaw === "object") for (const [k, v] of Object.entries(headersRaw)) {
39626
+ if (v === null || v === void 0) continue;
39627
+ if (typeof v === "string") headers[k] = v;
39628
+ else if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") headers[k] = String(v);
39629
+ else headers[k] = JSON.stringify(v) ?? "";
39630
+ }
39631
+ const result = {
39632
+ statusCode,
39633
+ headers
39634
+ };
39635
+ const cookiesRaw = obj["cookies"];
39636
+ if (Array.isArray(cookiesRaw)) result.cookies = cookiesRaw.map((c) => String(c));
39637
+ return result;
39638
+ }
39485
39639
 
39486
39640
  //#endregion
39487
39641
  //#region src/assets/asset-manifest-loader.ts
@@ -39795,6 +39949,398 @@ function resolveFnSubInvokeArn(arg) {
39795
39949
  };
39796
39950
  }
39797
39951
 
39952
+ //#endregion
39953
+ //#region src/local/httpv2-service-integration.ts
39954
+ const logger = getLogger();
39955
+ /**
39956
+ * Full list of subtypes cdkd recognizes as supported. Mirrors AWS docs.
39957
+ */
39958
+ const SUPPORTED_SUBTYPES = [
39959
+ "EventBridge-PutEvents",
39960
+ "SQS-SendMessage",
39961
+ "SQS-ReceiveMessage",
39962
+ "SQS-DeleteMessage",
39963
+ "SQS-PurgeQueue",
39964
+ "Kinesis-PutRecord",
39965
+ "StepFunctions-StartExecution",
39966
+ "StepFunctions-StartSyncExecution",
39967
+ "StepFunctions-StopExecution",
39968
+ "AppConfig-GetConfiguration"
39969
+ ];
39970
+ /**
39971
+ * Type guard: is the string an AWS-recognized service-integration subtype?
39972
+ *
39973
+ * Used by route discovery to classify routes — recognized subtypes go
39974
+ * through dispatch, anything else (typo, future-AWS-subtype-not-yet-supported)
39975
+ * falls back to deferred-501.
39976
+ */
39977
+ function isSupportedSubtype(value) {
39978
+ return typeof value === "string" && SUPPORTED_SUBTYPES.includes(value);
39979
+ }
39980
+ /**
39981
+ * Lazy-loaded SDK clients keyed by `<service>:<region>`. SDK packages
39982
+ * are heavyweight (~5-10 MB each); per-process per-region caching
39983
+ * avoids re-instantiating the AWS Signer + middleware stack on every
39984
+ * request.
39985
+ */
39986
+ const clientCache = /* @__PURE__ */ new Map();
39987
+ async function getClient(service, region) {
39988
+ const key = `${service}:${region}`;
39989
+ const cached = clientCache.get(key);
39990
+ if (cached) return cached;
39991
+ let client;
39992
+ switch (service) {
39993
+ case "sqs":
39994
+ client = new (await (import("@aws-sdk/client-sqs"))).SQSClient({ region });
39995
+ break;
39996
+ case "sns":
39997
+ client = new (await (import("@aws-sdk/client-sns"))).SNSClient({ region });
39998
+ break;
39999
+ case "eventbridge":
40000
+ client = new (await (import("@aws-sdk/client-eventbridge"))).EventBridgeClient({ region });
40001
+ break;
40002
+ case "kinesis":
40003
+ client = new (await (import("@aws-sdk/client-kinesis"))).KinesisClient({ region });
40004
+ break;
40005
+ case "sfn":
40006
+ client = new (await (import("@aws-sdk/client-sfn"))).SFNClient({ region });
40007
+ break;
40008
+ case "ssm":
40009
+ client = new (await (import("@aws-sdk/client-ssm"))).SSMClient({ region });
40010
+ break;
40011
+ default: throw new Error(`unknown service '${service}'`);
40012
+ }
40013
+ clientCache.set(key, client);
40014
+ return client;
40015
+ }
40016
+ /**
40017
+ * Dispatch a service integration: build the SDK input from the
40018
+ * pre-resolved parameter map, invoke the SDK, translate the response
40019
+ * to HTTP shape.
40020
+ *
40021
+ * `defaultRegion` is the cdkd process's default AWS region (from
40022
+ * `AWS_REGION` / profile / `--region`). When the resolved parameter
40023
+ * map includes a non-empty `Region`, that value overrides the default
40024
+ * for this single call — matches AWS API Gateway behavior.
40025
+ *
40026
+ * Returns a `ServiceIntegrationResult` for the HTTP server to write
40027
+ * to the client. SDK-level errors are caught and translated to
40028
+ * HTTP 4xx / 5xx — never thrown.
40029
+ */
40030
+ async function dispatchServiceIntegration(subtype, resolvedParameters, defaultRegion) {
40031
+ const region = (resolvedParameters["Region"] || defaultRegion).trim();
40032
+ if (!region) return errorResponse(400, "No AWS region configured. Set --region, AWS_REGION, or pass a 'Region' RequestParameter.");
40033
+ try {
40034
+ switch (subtype) {
40035
+ case "EventBridge-PutEvents": return await dispatchEventBridgePutEvents(resolvedParameters, region);
40036
+ case "SQS-SendMessage": return await dispatchSqsSendMessage(resolvedParameters, region);
40037
+ case "SQS-ReceiveMessage": return await dispatchSqsReceiveMessage(resolvedParameters, region);
40038
+ case "SQS-DeleteMessage": return await dispatchSqsDeleteMessage(resolvedParameters, region);
40039
+ case "SQS-PurgeQueue": return await dispatchSqsPurgeQueue(resolvedParameters, region);
40040
+ case "Kinesis-PutRecord": return await dispatchKinesisPutRecord(resolvedParameters, region);
40041
+ case "StepFunctions-StartExecution": return await dispatchSfnStartExecution(resolvedParameters, region);
40042
+ case "StepFunctions-StartSyncExecution": return await dispatchSfnStartSyncExecution(resolvedParameters, region);
40043
+ case "StepFunctions-StopExecution": return await dispatchSfnStopExecution(resolvedParameters, region);
40044
+ case "AppConfig-GetConfiguration": return await dispatchAppConfigGetConfiguration(resolvedParameters, region);
40045
+ }
40046
+ } catch (err) {
40047
+ return translateSdkError(subtype, err);
40048
+ }
40049
+ }
40050
+ async function dispatchEventBridgePutEvents(params, region) {
40051
+ requireParams(params, [
40052
+ "Detail",
40053
+ "DetailType",
40054
+ "Source"
40055
+ ]);
40056
+ const mod = await import("@aws-sdk/client-eventbridge");
40057
+ const client = await getClient("eventbridge", region);
40058
+ const entry = {
40059
+ Detail: params["Detail"],
40060
+ DetailType: params["DetailType"],
40061
+ Source: params["Source"]
40062
+ };
40063
+ if (params["Time"]) entry["Time"] = new Date(params["Time"]);
40064
+ if (params["EventBusName"]) entry["EventBusName"] = params["EventBusName"];
40065
+ if (params["Resources"]) entry["Resources"] = splitCsv(params["Resources"]);
40066
+ if (params["TraceHeader"]) entry["TraceHeader"] = params["TraceHeader"];
40067
+ return okJson(await client.send(new mod.PutEventsCommand({ Entries: [entry] })));
40068
+ }
40069
+ async function dispatchSqsSendMessage(params, region) {
40070
+ requireParams(params, ["QueueUrl", "MessageBody"]);
40071
+ const mod = await import("@aws-sdk/client-sqs");
40072
+ const client = await getClient("sqs", region);
40073
+ const input = {
40074
+ QueueUrl: params["QueueUrl"],
40075
+ MessageBody: params["MessageBody"]
40076
+ };
40077
+ if (params["DelaySeconds"]) input["DelaySeconds"] = Number(params["DelaySeconds"]);
40078
+ if (params["MessageDeduplicationId"]) input["MessageDeduplicationId"] = params["MessageDeduplicationId"];
40079
+ if (params["MessageGroupId"]) input["MessageGroupId"] = params["MessageGroupId"];
40080
+ if (params["MessageAttributes"]) input["MessageAttributes"] = parseJsonOrEmpty(params["MessageAttributes"]);
40081
+ if (params["MessageSystemAttributes"]) input["MessageSystemAttributes"] = parseJsonOrEmpty(params["MessageSystemAttributes"]);
40082
+ return okJson(await client.send(new mod.SendMessageCommand(input)));
40083
+ }
40084
+ async function dispatchSqsReceiveMessage(params, region) {
40085
+ requireParams(params, ["QueueUrl"]);
40086
+ const mod = await import("@aws-sdk/client-sqs");
40087
+ const client = await getClient("sqs", region);
40088
+ const input = { QueueUrl: params["QueueUrl"] };
40089
+ if (params["AttributeNames"]) input["AttributeNames"] = splitCsv(params["AttributeNames"]);
40090
+ if (params["MaxNumberOfMessages"]) input["MaxNumberOfMessages"] = Number(params["MaxNumberOfMessages"]);
40091
+ if (params["MessageAttributeNames"]) input["MessageAttributeNames"] = splitCsv(params["MessageAttributeNames"]);
40092
+ if (params["ReceiveRequestAttemptId"]) input["ReceiveRequestAttemptId"] = params["ReceiveRequestAttemptId"];
40093
+ if (params["VisibilityTimeout"]) input["VisibilityTimeout"] = Number(params["VisibilityTimeout"]);
40094
+ if (params["WaitTimeSeconds"]) input["WaitTimeSeconds"] = Number(params["WaitTimeSeconds"]);
40095
+ return okJson(await client.send(new mod.ReceiveMessageCommand(input)));
40096
+ }
40097
+ async function dispatchSqsDeleteMessage(params, region) {
40098
+ requireParams(params, ["QueueUrl", "ReceiptHandle"]);
40099
+ const mod = await import("@aws-sdk/client-sqs");
40100
+ return okJson(await (await getClient("sqs", region)).send(new mod.DeleteMessageCommand({
40101
+ QueueUrl: params["QueueUrl"],
40102
+ ReceiptHandle: params["ReceiptHandle"]
40103
+ })));
40104
+ }
40105
+ async function dispatchSqsPurgeQueue(params, region) {
40106
+ requireParams(params, ["QueueUrl"]);
40107
+ const mod = await import("@aws-sdk/client-sqs");
40108
+ return okJson(await (await getClient("sqs", region)).send(new mod.PurgeQueueCommand({ QueueUrl: params["QueueUrl"] })));
40109
+ }
40110
+ async function dispatchKinesisPutRecord(params, region) {
40111
+ requireParams(params, [
40112
+ "StreamName",
40113
+ "Data",
40114
+ "PartitionKey"
40115
+ ]);
40116
+ const mod = await import("@aws-sdk/client-kinesis");
40117
+ const client = await getClient("kinesis", region);
40118
+ const dataBytes = decodeBase64OrUtf8(params["Data"] ?? "");
40119
+ const input = {
40120
+ StreamName: params["StreamName"],
40121
+ Data: dataBytes,
40122
+ PartitionKey: params["PartitionKey"]
40123
+ };
40124
+ if (params["SequenceNumberForOrdering"]) input["SequenceNumberForOrdering"] = params["SequenceNumberForOrdering"];
40125
+ if (params["ExplicitHashKey"]) input["ExplicitHashKey"] = params["ExplicitHashKey"];
40126
+ return okJson(await client.send(new mod.PutRecordCommand(input)));
40127
+ }
40128
+ async function dispatchSfnStartExecution(params, region) {
40129
+ requireParams(params, ["StateMachineArn"]);
40130
+ const mod = await import("@aws-sdk/client-sfn");
40131
+ const client = await getClient("sfn", region);
40132
+ const input = { stateMachineArn: params["StateMachineArn"] };
40133
+ if (params["Name"]) input["name"] = params["Name"];
40134
+ if (params["Input"]) input["input"] = params["Input"];
40135
+ return okJson(await client.send(new mod.StartExecutionCommand(input)));
40136
+ }
40137
+ async function dispatchSfnStartSyncExecution(params, region) {
40138
+ requireParams(params, ["StateMachineArn"]);
40139
+ const mod = await import("@aws-sdk/client-sfn");
40140
+ const client = await getClient("sfn", region);
40141
+ const input = { stateMachineArn: params["StateMachineArn"] };
40142
+ if (params["Name"]) input["name"] = params["Name"];
40143
+ if (params["Input"]) input["input"] = params["Input"];
40144
+ if (params["TraceHeader"]) input["traceHeader"] = params["TraceHeader"];
40145
+ return okJson(await client.send(new mod.StartSyncExecutionCommand(input)));
40146
+ }
40147
+ async function dispatchSfnStopExecution(params, region) {
40148
+ requireParams(params, ["ExecutionArn"]);
40149
+ const mod = await import("@aws-sdk/client-sfn");
40150
+ const client = await getClient("sfn", region);
40151
+ const input = { executionArn: params["ExecutionArn"] };
40152
+ if (params["Cause"]) input["cause"] = params["Cause"];
40153
+ if (params["Error"]) input["error"] = params["Error"];
40154
+ return okJson(await client.send(new mod.StopExecutionCommand(input)));
40155
+ }
40156
+ async function dispatchAppConfigGetConfiguration(params, region) {
40157
+ return errorResponse(501, "AppConfig-GetConfiguration is recognized but cdkd does not yet bundle @aws-sdk/client-appconfig. Use the deployed API for this subtype, or open an issue if you need local emulation.");
40158
+ }
40159
+ function requireParams(params, required) {
40160
+ const missing = required.filter((k) => !params[k] || params[k].trim() === "");
40161
+ if (missing.length > 0) {
40162
+ const err = /* @__PURE__ */ new Error(`missing required RequestParameter(s): ${missing.join(", ")}`);
40163
+ err.statusCode = 400;
40164
+ throw err;
40165
+ }
40166
+ }
40167
+ function splitCsv(value) {
40168
+ return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40169
+ }
40170
+ function parseJsonOrEmpty(value) {
40171
+ try {
40172
+ const parsed = JSON.parse(value);
40173
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
40174
+ return {};
40175
+ } catch {
40176
+ return {};
40177
+ }
40178
+ }
40179
+ function decodeBase64OrUtf8(value) {
40180
+ const trimmed = value.trim();
40181
+ if (/^[A-Za-z0-9+/]+=*$/.test(trimmed) && trimmed.length % 4 === 0 && trimmed.length > 0) try {
40182
+ return Buffer.from(trimmed, "base64");
40183
+ } catch {}
40184
+ return Buffer.from(value, "utf8");
40185
+ }
40186
+ function okJson(response) {
40187
+ const stripped = stripSdkMetadata(response);
40188
+ return {
40189
+ statusCode: 200,
40190
+ body: JSON.stringify(stripped),
40191
+ headers: { "content-type": "application/json" }
40192
+ };
40193
+ }
40194
+ function stripSdkMetadata(obj) {
40195
+ if (obj === null || obj === void 0 || typeof obj !== "object") return obj;
40196
+ if (Array.isArray(obj)) return obj;
40197
+ const { $metadata: _meta, ...rest } = obj;
40198
+ return rest;
40199
+ }
40200
+ function errorResponse(statusCode, message) {
40201
+ return {
40202
+ statusCode,
40203
+ body: JSON.stringify({ message }),
40204
+ headers: { "content-type": "application/json" }
40205
+ };
40206
+ }
40207
+ /**
40208
+ * Translate an AWS SDK error to an HTTP response. AWS SDK v3 surfaces
40209
+ * errors as instances carrying `$metadata.httpStatusCode` + `name`;
40210
+ * we honor the status code when present, default to 500.
40211
+ */
40212
+ function translateSdkError(subtype, err) {
40213
+ if (err && typeof err === "object") {
40214
+ const e = err;
40215
+ const status = typeof e.statusCode === "number" && e.statusCode >= 100 && e.statusCode < 600 ? e.statusCode : e.$metadata?.httpStatusCode ?? 500;
40216
+ const body = {
40217
+ message: e.message ?? "AWS SDK call failed",
40218
+ code: e.name ?? "UnknownError"
40219
+ };
40220
+ logger.debug(`[${subtype}] SDK error (${status}): ${stringifyValue(body)}`);
40221
+ return {
40222
+ statusCode: status,
40223
+ body: JSON.stringify(body),
40224
+ headers: { "content-type": "application/json" }
40225
+ };
40226
+ }
40227
+ return errorResponse(500, `Unexpected error invoking ${subtype}: ${String(err)}`);
40228
+ }
40229
+ /**
40230
+ * Apply HTTP API v2 `ResponseParameters` mapping (per AWS docs:
40231
+ * https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html#http-api-mapping-supported-values).
40232
+ *
40233
+ * Keys are `<op>:header.<name>` or `overwrite:statuscode`. Values can
40234
+ * carry `$response.header.<name>`, `$context.<X>`, `$stageVariables.<X>`,
40235
+ * or be a static literal. JSONPath against `$response.body.X` is NOT
40236
+ * supported (would require SDK response parsing into the same shape
40237
+ * every subtype produces; deferred). Reserved headers (per AWS docs)
40238
+ * are rejected at this layer with a single-line debug log.
40239
+ *
40240
+ * Status-code lookup is by the SDK-returned `statusCode`. When the
40241
+ * exact code has no entry, the wildcard `'default'` entry is applied
40242
+ * if present (matches AWS deployed behavior).
40243
+ */
40244
+ function applyResponseParameters(base, responseParameters, responseCtx) {
40245
+ if (!responseParameters) return base;
40246
+ const overlay = responseParameters[String(base.statusCode)] ?? responseParameters["default"] ?? void 0;
40247
+ if (!overlay) return base;
40248
+ let statusCode = base.statusCode;
40249
+ const headers = { ...base.headers };
40250
+ for (const [key, value] of Object.entries(overlay)) {
40251
+ if (typeof value !== "string") continue;
40252
+ const resolved = resolveResponseValue(value, responseCtx, base);
40253
+ if (key === "overwrite:statuscode") {
40254
+ const next = Number(resolved);
40255
+ if (Number.isInteger(next) && next >= 100 && next < 600) statusCode = next;
40256
+ continue;
40257
+ }
40258
+ const headerMatch = /^(append|overwrite|remove):header\.(.+)$/i.exec(key);
40259
+ if (!headerMatch || !headerMatch[1] || !headerMatch[2]) continue;
40260
+ const op = headerMatch[1].toLowerCase();
40261
+ const name = headerMatch[2].toLowerCase();
40262
+ if (isReservedHeader(name)) {
40263
+ logger.debug(`ResponseParameters: header '${name}' is reserved by API Gateway and was skipped`);
40264
+ continue;
40265
+ }
40266
+ if (op === "remove") delete headers[name];
40267
+ else if (op === "overwrite") headers[name] = resolved;
40268
+ else if (op === "append") headers[name] = headers[name] ? `${headers[name]},${resolved}` : resolved;
40269
+ }
40270
+ return {
40271
+ statusCode,
40272
+ body: base.body,
40273
+ headers
40274
+ };
40275
+ }
40276
+ function resolveResponseValue(value, ctx, base) {
40277
+ if (value.startsWith("$") && !value.includes("${")) {
40278
+ const r = resolveSingleResponseRef(value, ctx, base);
40279
+ return r !== void 0 ? r : value;
40280
+ }
40281
+ if (value.includes("${")) {
40282
+ let out = "";
40283
+ let i = 0;
40284
+ while (i < value.length) {
40285
+ const next = value.indexOf("${", i);
40286
+ if (next === -1) {
40287
+ out += value.slice(i);
40288
+ break;
40289
+ }
40290
+ out += value.slice(i, next);
40291
+ const end = value.indexOf("}", next + 2);
40292
+ if (end === -1) return value;
40293
+ const r = resolveSingleResponseRef("$" + value.slice(next + 2, end), ctx, base);
40294
+ out += r ?? "";
40295
+ i = end + 1;
40296
+ }
40297
+ return out;
40298
+ }
40299
+ return value;
40300
+ }
40301
+ function resolveSingleResponseRef(ref, ctx, base) {
40302
+ if (ref.startsWith("$response.header.")) {
40303
+ const name = ref.substring(17).toLowerCase();
40304
+ return base.headers[name] ?? "";
40305
+ }
40306
+ if (ref.startsWith("$context.")) return ctx.context[ref.substring(9)] ?? "";
40307
+ if (ref.startsWith("$stageVariables.")) return ctx.stageVariables[ref.substring(16)] ?? "";
40308
+ }
40309
+ /**
40310
+ * Subset of AWS's reserved-headers list relevant to response mapping.
40311
+ * https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html#http-api-mapping-reserved-headers
40312
+ */
40313
+ const RESERVED_HEADER_PREFIXES = [
40314
+ "access-control-",
40315
+ "apigw-",
40316
+ "x-amz-",
40317
+ "x-amzn-"
40318
+ ];
40319
+ const RESERVED_HEADER_EXACT = [
40320
+ "authorization",
40321
+ "connection",
40322
+ "content-encoding",
40323
+ "content-length",
40324
+ "content-location",
40325
+ "forwarded",
40326
+ "keep-alive",
40327
+ "origin",
40328
+ "proxy-authenticate",
40329
+ "proxy-authorization",
40330
+ "te",
40331
+ "trailers",
40332
+ "transfer-encoding",
40333
+ "upgrade",
40334
+ "x-forwarded-for",
40335
+ "x-forwarded-host",
40336
+ "x-forwarded-proto",
40337
+ "via"
40338
+ ];
40339
+ function isReservedHeader(lowerName) {
40340
+ if (RESERVED_HEADER_EXACT.includes(lowerName)) return true;
40341
+ return RESERVED_HEADER_PREFIXES.some((p) => lowerName.startsWith(p));
40342
+ }
40343
+
39798
40344
  //#endregion
39799
40345
  //#region src/local/route-discovery.ts
39800
40346
  /**
@@ -40090,11 +40636,7 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
40090
40636
  lambdaLogicalId: "",
40091
40637
  unsupported: { reason: `${stackName}/${logicalId}: HTTP API v2 integration type '${String(integrationType)}' is not supported (only AWS_PROXY).` }
40092
40638
  }];
40093
- if (integrationProps["IntegrationSubtype"] !== void 0) return [{
40094
- ...baseRoute,
40095
- lambdaLogicalId: "",
40096
- unsupported: { reason: `${stackName}/${logicalId}: HTTP API v2 service integration with IntegrationSubtype '${stringifyValue(integrationProps["IntegrationSubtype"])}' is not supported (cdkd cannot proxy directly to SQS / EventBridge / etc.).` }
40097
- }];
40639
+ if (integrationProps["IntegrationSubtype"] !== void 0) return [classifyServiceIntegrationRoute(baseRoute, integrationProps, stackName, logicalId, integrationLogicalId)];
40098
40640
  const arnOutcome = resolveLambdaArnOutcome(integrationProps["IntegrationUri"]);
40099
40641
  if (arnOutcome.kind === "unsupported") return [{
40100
40642
  ...baseRoute,
@@ -40107,16 +40649,58 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
40107
40649
  }];
40108
40650
  }
40109
40651
  /**
40652
+ * Classify an HTTP API v2 route whose `IntegrationSubtype` is set
40653
+ * (service integration, no Lambda backing). Recognized subtypes
40654
+ * become `serviceIntegration` routes the HTTP server dispatches via
40655
+ * the SDK adapter table in `httpv2-service-integration.ts`;
40656
+ * unrecognized subtypes (typo / future-AWS-subtype-cdkd-doesn't-bundle
40657
+ * an SDK for) become deferred-501 unsupported routes.
40658
+ *
40659
+ * `RequestParameters` is the load-bearing field for dispatch — it
40660
+ * carries the SDK input as a flat map keyed by SDK parameter name.
40661
+ * Missing / non-object RequestParameters surfaces as unsupported (the
40662
+ * SDK call would have nothing to send).
40663
+ */
40664
+ function classifyServiceIntegrationRoute(baseRoute, integrationProps, stackName, routeLogicalId, integrationLogicalId) {
40665
+ const subtypeRaw = integrationProps["IntegrationSubtype"];
40666
+ const declaredAt = `${stackName}/${routeLogicalId}`;
40667
+ if (!isSupportedSubtype(subtypeRaw)) return {
40668
+ ...baseRoute,
40669
+ lambdaLogicalId: "",
40670
+ unsupported: { reason: `${declaredAt}: HTTP API v2 service integration subtype '${stringifyValue(subtypeRaw)}' is not supported by cdkd local start-api (see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html for the supported list).` }
40671
+ };
40672
+ const requestParameters = integrationProps["RequestParameters"];
40673
+ if (!requestParameters || typeof requestParameters !== "object" || Array.isArray(requestParameters)) return {
40674
+ ...baseRoute,
40675
+ lambdaLogicalId: "",
40676
+ unsupported: { reason: `${stackName}/${integrationLogicalId}: HTTP API v2 service integration '${subtypeRaw}' is missing RequestParameters or it is not an object — cannot dispatch to the SDK without input mapping.` }
40677
+ };
40678
+ const responseParameters = integrationProps["ResponseParameters"];
40679
+ const validatedResponseParameters = responseParameters && typeof responseParameters === "object" && !Array.isArray(responseParameters) ? responseParameters : void 0;
40680
+ return {
40681
+ ...baseRoute,
40682
+ lambdaLogicalId: "",
40683
+ serviceIntegration: {
40684
+ subtype: subtypeRaw,
40685
+ requestParameters,
40686
+ ...validatedResponseParameters && { responseParameters: validatedResponseParameters }
40687
+ }
40688
+ };
40689
+ }
40690
+ /**
40110
40691
  * Discover the synthetic `ANY /{proxy+}` route from an
40111
40692
  * `AWS::Lambda::Url` resource.
40112
40693
  *
40113
40694
  * Per-shape classification:
40114
- * - `AuthType === 'NONE'` + `InvokeMode !== 'RESPONSE_STREAM'` → normal route.
40695
+ * - `AuthType === 'NONE'` + `InvokeMode === 'BUFFERED'` (or unset) → normal route.
40696
+ * - `AuthType === 'NONE'` + `InvokeMode === 'RESPONSE_STREAM'` → normal route
40697
+ * dispatched via the RIE streaming protocol (the response body is a
40698
+ * JSON prelude — `{statusCode, headers, cookies?}` — followed by 8
40699
+ * NULL bytes and then the raw body chunks). The HTTP server pipes
40700
+ * the chunks to the client with `Transfer-Encoding: chunked` (#467).
40115
40701
  * - `AuthType !== 'NONE'` (e.g. `AWS_IAM`) → deferred-error
40116
40702
  * unsupported. Boot proceeds; HTTP 501 + `reason` at request time.
40117
40703
  * IAM auth would need SigV4 verification cdkd cannot emulate.
40118
- * - `InvokeMode === 'RESPONSE_STREAM'` → deferred-error unsupported.
40119
- * The RIE container does not implement `InvokeWithResponseStream`.
40120
40704
  *
40121
40705
  * The Lambda Arn intrinsic resolution still **hard-errors** when it
40122
40706
  * cannot pin down a same-template Lambda — Function URLs have no other
@@ -40146,14 +40730,18 @@ function discoverFunctionUrl(logicalId, resource, template, stackName) {
40146
40730
  lambdaLogicalId,
40147
40731
  unsupported: { reason: `${stackName}/${logicalId}: AuthType '${String(authType)}' is not supported (only NONE — IAM auth requires SigV4 verification cdkd cannot emulate locally).` }
40148
40732
  }];
40149
- if (props["InvokeMode"] === "RESPONSE_STREAM") return [{
40733
+ const invokeModeRaw = props["InvokeMode"];
40734
+ let invokeMode = "BUFFERED";
40735
+ if (invokeModeRaw === "RESPONSE_STREAM") invokeMode = "RESPONSE_STREAM";
40736
+ else if (invokeModeRaw !== void 0 && invokeModeRaw !== "BUFFERED") return [{
40150
40737
  ...baseRoute,
40151
40738
  lambdaLogicalId,
40152
- unsupported: { reason: `${stackName}/${logicalId}: InvokeMode RESPONSE_STREAM is not supported (cdkd's RIE container does not implement InvokeWithResponseStream).` }
40739
+ unsupported: { reason: `${stackName}/${logicalId}: InvokeMode ${shortJson$1(invokeModeRaw)} is not a recognized value (expected 'BUFFERED' or 'RESPONSE_STREAM').` }
40153
40740
  }];
40154
40741
  return [{
40155
40742
  ...baseRoute,
40156
- lambdaLogicalId
40743
+ lambdaLogicalId,
40744
+ invokeMode
40157
40745
  }];
40158
40746
  }
40159
40747
  /**
@@ -41761,7 +42349,7 @@ function attachAuthorizers(stacks, routes) {
41761
42349
  const out = [];
41762
42350
  const errors = [];
41763
42351
  for (const route of routes) {
41764
- if (route.unsupported || route.mockCors) {
42352
+ if (route.unsupported || route.mockCors || route.serviceIntegration) {
41765
42353
  out.push({ route });
41766
42354
  continue;
41767
42355
  }
@@ -41791,6 +42379,45 @@ function attachAuthorizers(stacks, routes) {
41791
42379
  return out;
41792
42380
  }
41793
42381
  /**
42382
+ * Walk every discovered route and return the service-integration routes
42383
+ * whose source CFn resource declares an authorizer (HTTP API v2 routes
42384
+ * with `AuthorizationType !== 'NONE'`). Service-integration routes only
42385
+ * exist on `AWS::ApiGatewayV2::Route`; REST v1 service integrations are
42386
+ * a different shape and not yet supported.
42387
+ */
42388
+ function findIgnoredServiceIntegrationAuthorizers(stacks, routes) {
42389
+ const stackByRoute = /* @__PURE__ */ new Map();
42390
+ for (const stack of stacks) {
42391
+ const prefix = `${stack.stackName}/`;
42392
+ for (const route of routes) if (route.declaredAt.startsWith(prefix)) stackByRoute.set(route.declaredAt, stack);
42393
+ }
42394
+ const out = [];
42395
+ for (const route of routes) {
42396
+ if (!route.serviceIntegration) continue;
42397
+ const stack = stackByRoute.get(route.declaredAt);
42398
+ if (!stack) continue;
42399
+ const slash = route.declaredAt.indexOf("/");
42400
+ if (slash < 0) continue;
42401
+ const logicalId = route.declaredAt.slice(slash + 1);
42402
+ const resource = stack.template.Resources?.[logicalId];
42403
+ if (!resource || resource.Type !== "AWS::ApiGatewayV2::Route") continue;
42404
+ const props = resource.Properties ?? {};
42405
+ const authType = props["AuthorizationType"];
42406
+ if (typeof authType !== "string" || authType === "" || authType.toUpperCase() === "NONE") continue;
42407
+ let authorizerName;
42408
+ if (authType === "AWS_IAM") authorizerName = "AWS_IAM";
42409
+ else {
42410
+ const ref = props["AuthorizerId"];
42411
+ authorizerName = (ref && typeof ref === "object" && "Ref" in ref && typeof ref.Ref === "string" ? ref.Ref : void 0) ?? `<authType=${authType}, AuthorizerId malformed>`;
42412
+ }
42413
+ out.push({
42414
+ declaredAt: route.declaredAt,
42415
+ authorizerName
42416
+ });
42417
+ }
42418
+ return out;
42419
+ }
42420
+ /**
41794
42421
  * Detect the authorizer (if any) attached to a discovered route.
41795
42422
  * Walks the original CFn resource for the route in `stack.template`.
41796
42423
  */
@@ -42920,6 +43547,191 @@ function amzDateOutsideSkew(amzDate, now) {
42920
43547
  return Math.abs(ts.getTime() - now.getTime()) > 900 * 1e3;
42921
43548
  }
42922
43549
 
43550
+ //#endregion
43551
+ //#region src/local/parameter-mapping.ts
43552
+ /**
43553
+ * Parameter-mapping resolver for HTTP API v2 service integrations
43554
+ * (`IntegrationSubtype` set, no Lambda).
43555
+ *
43556
+ * AWS API Gateway HTTP APIs let you wire a route directly to an AWS
43557
+ * SDK call via `RequestParameters` — a flat map whose KEY is the SDK
43558
+ * input parameter name (`QueueUrl`, `MessageBody`, `Source`, ...) and
43559
+ * whose VALUE is either a literal string or one of the dollar-prefixed
43560
+ * "selection expression" forms documented at
43561
+ * https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html
43562
+ *
43563
+ * Supported value forms (all bare and `${...}`-wrapped variants):
43564
+ * - `$request.header.<name>` — case-insensitive header lookup; multi-values comma-joined
43565
+ * - `$request.querystring.<key>` — case-SENSITIVE; multi-values comma-joined
43566
+ * - `$request.path.<param>` — path parameter from `{param}` route patterns
43567
+ * - `$request.path` — full request path (without stage)
43568
+ * - `$request.body` — entire request body as a string
43569
+ * - `$request.body.<jsonpath>` — JSONPath against the parsed JSON body
43570
+ * (recursive descent `..` and filter `?()` are NOT supported)
43571
+ * - `$context.<key>` — supported context variables
43572
+ * (requestId / accountId / domainName / identity.sourceIp / etc.)
43573
+ * - `$stageVariables.<key>` — values from the route's selected stage
43574
+ *
43575
+ * `${X} ${Y}` interpolation: any string containing `${...}` is treated
43576
+ * as a template literal — each placeholder is resolved independently
43577
+ * and the surrounding literal characters are preserved.
43578
+ *
43579
+ * Anything that is not a recognized selection expression AND does not
43580
+ * contain `${...}` is returned as a literal string.
43581
+ *
43582
+ * This module is **pure-functional and async-free** — every input is
43583
+ * derived from the HTTP request snapshot, route match, and the
43584
+ * pre-discovered route state. SDK invocation happens in the dispatcher
43585
+ * (see `httpv2-service-integration.ts`).
43586
+ *
43587
+ * Unresolved references (e.g. `$request.querystring.url` against a
43588
+ * request with no `url` parameter) resolve to the empty string —
43589
+ * matches deployed API Gateway behavior, which treats absent values
43590
+ * as `""` and passes them to the SDK call. The dispatcher's
43591
+ * per-subtype validator then surfaces the SDK's typed rejection.
43592
+ */
43593
+ /**
43594
+ * Resolve a `RequestParameters` map (HTTP API v2 service integration
43595
+ * shape) against the incoming HTTP request. Keys are passed through
43596
+ * verbatim (they identify the SDK input parameter); only the VALUES
43597
+ * go through selection-expression resolution.
43598
+ */
43599
+ function resolveServiceIntegrationParameters(parameters, ctx) {
43600
+ const resolved = {};
43601
+ for (const [key, rawValue] of Object.entries(parameters)) {
43602
+ if (typeof rawValue !== "string") return {
43603
+ kind: "error",
43604
+ reason: `RequestParameters[${JSON.stringify(key)}] must be a string (got ${typeof rawValue}: ${stringifyValue(rawValue)}).`
43605
+ };
43606
+ try {
43607
+ resolved[key] = resolveSelectionExpression(rawValue, ctx);
43608
+ } catch (err) {
43609
+ return {
43610
+ kind: "error",
43611
+ reason: `RequestParameters[${JSON.stringify(key)}]: ${err instanceof Error ? err.message : String(err)}`
43612
+ };
43613
+ }
43614
+ }
43615
+ return {
43616
+ kind: "ok",
43617
+ resolved
43618
+ };
43619
+ }
43620
+ /**
43621
+ * Resolve a single selection-expression string. Public for unit
43622
+ * testing — production callers use {@link resolveServiceIntegrationParameters}.
43623
+ *
43624
+ * Three shapes:
43625
+ * 1. Pure bare reference (`$request.querystring.url`) — resolved
43626
+ * and returned as-is. Whole-string match.
43627
+ * 2. Embedded `${...}` interpolation — every placeholder is resolved
43628
+ * and concatenated with the surrounding literals.
43629
+ * 3. Anything else — returned verbatim as a literal.
43630
+ */
43631
+ function resolveSelectionExpression(input, ctx) {
43632
+ if (input.startsWith("$") && !input.includes("${")) {
43633
+ const resolved = resolveSingleReference(input, ctx);
43634
+ if (resolved !== void 0) return resolved;
43635
+ return input;
43636
+ }
43637
+ if (input.includes("${")) return interpolate(input, ctx);
43638
+ return input;
43639
+ }
43640
+ /**
43641
+ * Walk a `${...}`-templated string and emit the concatenated result.
43642
+ * Per AWS docs, `${X}` may contain any of the selection-expression
43643
+ * forms recognized in bare form.
43644
+ */
43645
+ function interpolate(input, ctx) {
43646
+ let out = "";
43647
+ let i = 0;
43648
+ while (i < input.length) {
43649
+ const next = input.indexOf("${", i);
43650
+ if (next === -1) {
43651
+ out += input.slice(i);
43652
+ break;
43653
+ }
43654
+ out += input.slice(i, next);
43655
+ const end = input.indexOf("}", next + 2);
43656
+ if (end === -1) throw new Error(`unclosed '\${...}' interpolation in selection expression`);
43657
+ const resolved = resolveSingleReference("$" + input.slice(next + 2, end), ctx);
43658
+ out += resolved ?? "";
43659
+ i = end + 1;
43660
+ }
43661
+ return out;
43662
+ }
43663
+ /**
43664
+ * Resolve one selection-expression reference (without `${...}`
43665
+ * wrapping). Returns `undefined` when the reference's PREFIX is not
43666
+ * a recognized form (so the bare-form caller can fall through to
43667
+ * literal-pass-through); returns `""` when the prefix matched but
43668
+ * the referenced datum was absent (AWS-deployed behavior).
43669
+ */
43670
+ function resolveSingleReference(ref, ctx) {
43671
+ if (ref === "$request.body") return ctx.body;
43672
+ if (ref === "$request.path") return ctx.requestPath;
43673
+ if (ref.startsWith("$request.body.")) {
43674
+ const path = ref.substring(14);
43675
+ return resolveBodyJsonPath(ctx.body, path);
43676
+ }
43677
+ if (ref.startsWith("$request.header.")) {
43678
+ const name = ref.substring(16).toLowerCase();
43679
+ return ctx.headers[name] ?? "";
43680
+ }
43681
+ if (ref.startsWith("$request.querystring.")) {
43682
+ const key = ref.substring(21);
43683
+ return ctx.queryString[key] ?? "";
43684
+ }
43685
+ if (ref.startsWith("$request.path.")) {
43686
+ const key = ref.substring(14);
43687
+ return ctx.pathParameters[key] ?? "";
43688
+ }
43689
+ if (ref.startsWith("$context.")) {
43690
+ const key = ref.substring(9);
43691
+ return ctx.context[key] ?? "";
43692
+ }
43693
+ if (ref.startsWith("$stageVariables.")) {
43694
+ const key = ref.substring(16);
43695
+ return ctx.stageVariables[key] ?? "";
43696
+ }
43697
+ }
43698
+ /**
43699
+ * Resolve a simple JSON path against the request body. Per AWS docs:
43700
+ *
43701
+ * - The body is JSON-parsed (best-effort; non-JSON → empty string).
43702
+ * - Path segments are `.`-separated. Array indexing via `[N]` is
43703
+ * supported.
43704
+ * - Recursive descent `..` and filter expressions `?()` are NOT
43705
+ * supported and produce `""` (matches AWS rejection at deploy
43706
+ * time, but we degrade gracefully at runtime).
43707
+ *
43708
+ * Non-string leaves are stringified with `JSON.stringify` so they can
43709
+ * round-trip into the SDK call's string-typed input fields.
43710
+ */
43711
+ function resolveBodyJsonPath(body, path) {
43712
+ if (path.startsWith(".") || path.includes("..") || path.includes("?(")) return "";
43713
+ let parsed;
43714
+ try {
43715
+ parsed = JSON.parse(body);
43716
+ } catch {
43717
+ return "";
43718
+ }
43719
+ const segments = path.split(/\.|\[(\d+)\]/).filter((s) => s !== void 0 && s !== "");
43720
+ let cursor = parsed;
43721
+ for (const seg of segments) {
43722
+ if (cursor === null || cursor === void 0) return "";
43723
+ if (typeof cursor !== "object") return "";
43724
+ if (Array.isArray(cursor)) {
43725
+ const idx = Number(seg);
43726
+ if (!Number.isInteger(idx)) return "";
43727
+ cursor = cursor[idx];
43728
+ } else cursor = cursor[seg];
43729
+ }
43730
+ if (cursor === void 0 || cursor === null) return "";
43731
+ if (typeof cursor === "string") return cursor;
43732
+ return JSON.stringify(cursor);
43733
+ }
43734
+
42923
43735
  //#endregion
42924
43736
  //#region src/local/http-server.ts
42925
43737
  /**
@@ -43020,6 +43832,10 @@ async function handleRequest(req, res, state, opts) {
43020
43832
  writeNotImplemented(res, match.route.unsupported.reason);
43021
43833
  return;
43022
43834
  }
43835
+ if (match.route.serviceIntegration) {
43836
+ await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts);
43837
+ return;
43838
+ }
43023
43839
  const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
43024
43840
  const clientCert = opts.mtls ? extractClientCert(req) : void 0;
43025
43841
  const snapshot = {
@@ -43062,6 +43878,26 @@ async function handleRequest(req, res, state, opts) {
43062
43878
  writeError(res, 502);
43063
43879
  return;
43064
43880
  }
43881
+ if (match.route.invokeMode === "RESPONSE_STREAM") {
43882
+ let streamResult;
43883
+ try {
43884
+ streamResult = await invokeRieStreaming(handle.containerHost, handle.hostPort, baseEvent, opts.rieTimeoutMs);
43885
+ try {
43886
+ writeStreamingResponse(res, streamResult, () => state.pool.release(handle));
43887
+ } catch (writeErr) {
43888
+ streamResult.body.on("error", () => {});
43889
+ streamResult.body.destroy(writeErr instanceof Error ? writeErr : new Error(String(writeErr)));
43890
+ throw writeErr;
43891
+ }
43892
+ return;
43893
+ } catch (err) {
43894
+ logger.error(`RIE streaming invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
43895
+ if (!res.headersSent) writeError(res, 502);
43896
+ else res.end();
43897
+ state.pool.release(handle);
43898
+ return;
43899
+ }
43900
+ }
43065
43901
  try {
43066
43902
  const translated = translateLambdaResponse((await invokeRie(handle.containerHost, handle.hostPort, baseEvent, opts.rieTimeoutMs)).payload, match.route.apiVersion);
43067
43903
  res.statusCode = translated.statusCode;
@@ -43077,6 +43913,42 @@ async function handleRequest(req, res, state, opts) {
43077
43913
  }
43078
43914
  }
43079
43915
  /**
43916
+ * Pipe a streaming RIE response into an `http.ServerResponse`. The
43917
+ * prelude's status + headers are written via `res.writeHead(...)`; the
43918
+ * body Readable is `pipe`'d through to the response so Node's
43919
+ * `Transfer-Encoding: chunked` machinery handles backpressure +
43920
+ * chunked framing automatically.
43921
+ *
43922
+ * `releasePool` runs in a `finally`-equivalent path so the warm
43923
+ * container is returned to the pool whether the stream ends cleanly
43924
+ * (`'end'` event) or errors mid-body (`'error'` event). Errors after
43925
+ * the prelude has been written can no longer be reported as HTTP
43926
+ * status — the stream is destroyed and the connection aborts.
43927
+ *
43928
+ * Cookies in the prelude are emitted as multiple `Set-Cookie` headers
43929
+ * (HTTP API v2 semantics — matching the buffered path's behavior).
43930
+ */
43931
+ function writeStreamingResponse(res, result, releasePool) {
43932
+ const logger = getLogger().child("start-api");
43933
+ const { prelude, body } = result;
43934
+ const headersOut = { ...prelude.headers };
43935
+ if (prelude.cookies && prelude.cookies.length > 0) headersOut["set-cookie"] = prelude.cookies;
43936
+ res.writeHead(prelude.statusCode, headersOut);
43937
+ let released = false;
43938
+ const releaseOnce = () => {
43939
+ if (released) return;
43940
+ released = true;
43941
+ releasePool();
43942
+ };
43943
+ body.on("error", (err) => {
43944
+ logger.error(`Streaming Lambda response body errored mid-stream: ${err instanceof Error ? err.message : String(err)}`);
43945
+ res.destroy(err);
43946
+ releaseOnce();
43947
+ });
43948
+ res.on("close", releaseOnce);
43949
+ body.pipe(res);
43950
+ }
43951
+ /**
43080
43952
  * Attempt CORS preflight interception. Returns `true` when the
43081
43953
  * preflight response was written (caller must NOT continue to route
43082
43954
  * dispatch); `false` when no preflight match (caller falls through to
@@ -43397,9 +44269,12 @@ function lowercaseSingularHeaders(raw) {
43397
44269
  return out;
43398
44270
  }
43399
44271
  /**
43400
- * Parse query string into a singular (last-wins) map. Used by the
43401
- * authorizer pass separate from api-gateway-event because the
43402
- * authorizer needs raw header values too.
44272
+ * Parse query string into a singular map. Multi-value keys are
44273
+ * comma-joined in order of appearance (`?foo=a&foo=b` -> `foo: 'a,b'`)
44274
+ * matches the contract documented at
44275
+ * `src/local/parameter-mapping.ts:14` ("multi-values comma-joined")
44276
+ * AND deployed API Gateway behavior. Used by both the authorizer pass
44277
+ * and the HTTP API v2 service-integration parameter mapper.
43403
44278
  */
43404
44279
  function parseQueryStringSingular(rawUrl) {
43405
44280
  const q = rawUrl.indexOf("?");
@@ -43420,7 +44295,8 @@ function parseQueryStringSingular(rawUrl) {
43420
44295
  try {
43421
44296
  value = decodeURIComponent(rawValue);
43422
44297
  } catch {}
43423
- out[key] = value;
44298
+ const prev = out[key];
44299
+ out[key] = prev === void 0 ? value : `${prev},${value}`;
43424
44300
  }
43425
44301
  return out;
43426
44302
  }
@@ -43466,6 +44342,120 @@ function writeError(res, statusCode, body = "{\"message\":\"Internal server erro
43466
44342
  res.end(body);
43467
44343
  }
43468
44344
  /**
44345
+ * Handle an HTTP API v2 service-integration route (#458). The route
44346
+ * carries `serviceIntegration: { subtype, requestParameters, responseParameters? }`
44347
+ * — no Lambda backs it. Flow:
44348
+ *
44349
+ * 1. Build a {@link RequestParameterContext} from the HTTP request +
44350
+ * route match (path parameters, query string, headers, context
44351
+ * variables, stage variables).
44352
+ * 2. Resolve every `RequestParameters` value against that context via
44353
+ * `parameter-mapping.ts`. Per-parameter unresolved refs degrade to
44354
+ * `""` (matches AWS deployed behavior).
44355
+ * 3. Dispatch to the per-subtype SDK adapter in
44356
+ * `httpv2-service-integration.ts`. Per-request `Region` parameter
44357
+ * overrides the server's `opts.defaultRegion`; absent both surfaces
44358
+ * a 400.
44359
+ * 4. Apply per-status-code `ResponseParameters` overlay (header /
44360
+ * statuscode overwrites).
44361
+ * 5. Write the result to the HTTP client.
44362
+ *
44363
+ * Authorizer pass: NOT yet wired (current scope is dispatch only). When
44364
+ * a service-integration route carries an authorizer, the discovery layer
44365
+ * leaves it in place but the server short-circuits to dispatch BEFORE the
44366
+ * auth pass. A follow-up PR can hoist the auth pass earlier — keeping it
44367
+ * out of this PR limits the blast radius.
44368
+ */
44369
+ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
44370
+ const route = match.route;
44371
+ const svc = route.serviceIntegration;
44372
+ if (!svc) {
44373
+ writeError(res, 500);
44374
+ return;
44375
+ }
44376
+ const rawUrl = req.url ?? "/";
44377
+ const headersFlat = flattenHeadersForMapping(req);
44378
+ const queryString = parseQueryStringSingular(rawUrl);
44379
+ const requestPath = rawUrl.split("?")[0] ?? "/";
44380
+ const context = buildServiceIntegrationContextVars(req, route);
44381
+ const ctx = {
44382
+ headers: headersFlat,
44383
+ queryString,
44384
+ pathParameters: match.pathParameters,
44385
+ requestPath,
44386
+ body: bodyBuf.toString("utf8"),
44387
+ context,
44388
+ stageVariables: route.stageVariables ?? {}
44389
+ };
44390
+ const outcome = resolveServiceIntegrationParameters(svc.requestParameters, ctx);
44391
+ if (outcome.kind === "error") {
44392
+ getLogger().warn(`[${route.declaredAt}] ${outcome.reason}`);
44393
+ const body = JSON.stringify({
44394
+ message: "Invalid integration mapping",
44395
+ reason: outcome.reason
44396
+ });
44397
+ res.statusCode = 500;
44398
+ res.setHeader("content-type", "application/json");
44399
+ res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
44400
+ res.end(body);
44401
+ return;
44402
+ }
44403
+ const result = await dispatchServiceIntegration(svc.subtype, outcome.resolved, opts.defaultRegion ?? "");
44404
+ const responseCtx = {
44405
+ context,
44406
+ stageVariables: route.stageVariables ?? {}
44407
+ };
44408
+ const finalResult = applyResponseParameters(result, svc.responseParameters, responseCtx);
44409
+ res.statusCode = finalResult.statusCode;
44410
+ for (const [name, value] of Object.entries(finalResult.headers)) res.setHeader(name, value);
44411
+ res.setHeader("content-length", String(Buffer.byteLength(finalResult.body, "utf-8")));
44412
+ res.end(finalResult.body);
44413
+ }
44414
+ /**
44415
+ * Flatten Node's `req.headers` to the single-string-per-key shape the
44416
+ * parameter-mapping resolver expects (`$request.header.<name>` is
44417
+ * documented as comma-joined multi-value). Header NAMES are lowercased
44418
+ * because AWS docs document `$request.header.<name>` as case-insensitive.
44419
+ */
44420
+ function flattenHeadersForMapping(req) {
44421
+ const out = {};
44422
+ for (const [name, value] of Object.entries(req.headers)) {
44423
+ const lower = name.toLowerCase();
44424
+ if (Array.isArray(value)) out[lower] = value.join(", ");
44425
+ else if (typeof value === "string") out[lower] = value;
44426
+ }
44427
+ return out;
44428
+ }
44429
+ /**
44430
+ * Build the subset of AWS context variables the service-integration
44431
+ * parameter-mapping resolver needs to surface (per AWS docs
44432
+ * https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html).
44433
+ *
44434
+ * We populate the most-commonly-referenced fields with realistic values
44435
+ * (`requestId` is fresh per call, `accountId` / `domainName` are mock
44436
+ * but stable). Selection expressions against context variables we
44437
+ * don't model resolve to `""` — matches the AWS-deployed behavior of
44438
+ * absent values.
44439
+ */
44440
+ function buildServiceIntegrationContextVars(req, route) {
44441
+ const requestId = `cdkd-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
44442
+ const sourceIp = req.socket.remoteAddress ?? "127.0.0.1";
44443
+ return {
44444
+ requestId,
44445
+ accountId: "123456789012",
44446
+ apiId: route.apiLogicalId ?? "local",
44447
+ stage: route.stage,
44448
+ "identity.sourceIp": sourceIp,
44449
+ "identity.userAgent": Array.isArray(req.headers["user-agent"]) || typeof req.headers["user-agent"] === "string" ? String(req.headers["user-agent"]) : "",
44450
+ domainName: "localhost",
44451
+ httpMethod: req.method ?? "GET",
44452
+ path: (req.url ?? "/").split("?")[0] ?? "/",
44453
+ protocol: "HTTP/1.1",
44454
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
44455
+ requestTimeEpoch: String(Date.now())
44456
+ };
44457
+ }
44458
+ /**
43469
44459
  * Write the 501 Not Implemented response surfaced for routes the
43470
44460
  * discovery layer flagged as `unsupported`. The integration's reason
43471
44461
  * (e.g. "MOCK integration is not emulated", "WebSocket APIs are not
@@ -44227,6 +45217,7 @@ async function localStartApiCommand(target, options) {
44227
45217
  warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
44228
45218
  sigV4CredentialsLoader = defaultCredentialsLoader();
44229
45219
  warnIamRoutes(initialMaterial.routes);
45220
+ warnIgnoredServiceIntegrationAuthorizers(initialMaterial.routes, initialMaterial.stacks ?? []);
44230
45221
  let maxTimeoutSec = 0;
44231
45222
  for (const spec of initialMaterial.specs.values()) if (spec.lambda.timeoutSec > maxTimeoutSec) maxTimeoutSec = spec.lambda.timeoutSec;
44232
45223
  const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
@@ -44251,6 +45242,7 @@ async function localStartApiCommand(target, options) {
44251
45242
  for (const result of handles) if (result.status === "fulfilled") groupPool.release(result.value);
44252
45243
  else logger.warn(`Pre-warm failed for one Lambda in ${group.displayName} (cold start cost will apply on first request): ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
44253
45244
  }
45245
+ const defaultRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? void 0;
44254
45246
  const started = await startApiServer({
44255
45247
  state: groupState,
44256
45248
  rieTimeoutMs,
@@ -44262,7 +45254,8 @@ async function localStartApiCommand(target, options) {
44262
45254
  jwksWarnedUrls,
44263
45255
  sigV4CredentialsLoader,
44264
45256
  sigV4WarnedForeignIds,
44265
- sigV4AllowUnverified: options.allowUnverifiedSigv4 === true
45257
+ sigV4AllowUnverified: options.allowUnverifiedSigv4 === true,
45258
+ ...defaultRegion && { defaultRegion }
44266
45259
  });
44267
45260
  servers.push({
44268
45261
  group,
@@ -44391,7 +45384,7 @@ function uniqueLambdaIds(routes, routesWithAuth) {
44391
45384
  const seen = /* @__PURE__ */ new Set();
44392
45385
  const out = [];
44393
45386
  for (const r of routes) {
44394
- if (r.unsupported || r.mockCors) continue;
45387
+ if (r.unsupported || r.mockCors || r.serviceIntegration) continue;
44395
45388
  if (r.lambdaLogicalId.length === 0) continue;
44396
45389
  if (!seen.has(r.lambdaLogicalId)) {
44397
45390
  seen.add(r.lambdaLogicalId);
@@ -44399,7 +45392,7 @@ function uniqueLambdaIds(routes, routesWithAuth) {
44399
45392
  }
44400
45393
  }
44401
45394
  for (const entry of routesWithAuth) {
44402
- if (entry.route.unsupported || entry.route.mockCors) continue;
45395
+ if (entry.route.unsupported || entry.route.mockCors || entry.route.serviceIntegration) continue;
44403
45396
  const auth = entry.authorizer;
44404
45397
  if (!auth) continue;
44405
45398
  if (auth.kind === "lambda-token" || auth.kind === "lambda-request") {
@@ -44484,6 +45477,27 @@ function warnIamRoutes(routesWithAuth) {
44484
45477
  return true;
44485
45478
  }
44486
45479
  /**
45480
+ * #458 / PR #500 review: emit a one-line warn naming every service-
45481
+ * integration route whose source CFn resource declares an authorizer
45482
+ * (HTTP API v2 routes with `AuthorizationType !== 'NONE'`). The
45483
+ * dispatcher in `http-server.ts` runs the SDK call BEFORE the
45484
+ * authorizer pass would fire, so without this warn a CDK app that
45485
+ * wires JWT / Lambda / Cognito / IAM authorizers onto service
45486
+ * integrations would silently let every local request reach the SDK
45487
+ * call without auth. Threading the auth pass through the
45488
+ * service-integration dispatcher is a follow-up issue. Returns the
45489
+ * number of warned routes so tests can assert the firing path; the
45490
+ * value is otherwise unused.
45491
+ */
45492
+ function warnIgnoredServiceIntegrationAuthorizers(routesWithAuth, stacks) {
45493
+ const logger = getLogger();
45494
+ const ignored = findIgnoredServiceIntegrationAuthorizers(stacks, routesWithAuth.map((entry) => entry.route));
45495
+ if (ignored.length === 0) return 0;
45496
+ logger.warn(`${ignored.length} HTTP API v2 service-integration route(s) declare an authorizer but cdkd local start-api dispatches the SDK call BEFORE the authorizer pass — every local request reaches the SDK call WITHOUT authentication. This is a deferred feature; see https://github.com/go-to-k/cdkd/issues/502 for the follow-up tracking issue.`);
45497
+ for (const entry of ignored) logger.warn(` - ${entry.declaredAt}: authorizer '${entry.authorizerName}' is configured but ignored`);
45498
+ return ignored.length;
45499
+ }
45500
+ /**
44487
45501
  * Build the per-Lambda container spec — code dir, env vars (template +
44488
45502
  * --env-vars overlay), STS-issued creds when --assume-role names this
44489
45503
  * Lambda, optional --debug-port reservation. Errors out with a clear
@@ -44799,7 +45813,7 @@ function printRouteTable(routes) {
44799
45813
  process.stdout.write("Discovered routes:\n");
44800
45814
  for (const r of sorted) {
44801
45815
  const sourceLabel = r.source === "http-api" ? "HTTP API" : r.source === "rest-v1" ? `REST v1, stage '${r.stage}'` : "Function URL";
44802
- const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.lambdaLogicalId;
45816
+ const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.serviceIntegration ? `[${r.serviceIntegration.subtype}]` : r.lambdaLogicalId;
44803
45817
  process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${target} (${sourceLabel})\n`);
44804
45818
  }
44805
45819
  process.stdout.write("\n");
@@ -48297,7 +49311,7 @@ function reorderArgs(argv) {
48297
49311
  */
48298
49312
  async function main() {
48299
49313
  const program = new Command();
48300
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.128.0");
49314
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.130.0");
48301
49315
  program.addCommand(createBootstrapCommand());
48302
49316
  program.addCommand(createSynthCommand());
48303
49317
  program.addCommand(createListCommand());