cdk-local 0.44.0 → 0.45.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.
@@ -7794,7 +7794,7 @@ async function localInvokeCommand(target, options, extraStateProviders) {
7794
7794
  }
7795
7795
  }
7796
7796
  if (!assumeSucceeded) {
7797
- forwardAwsEnv$2(dockerEnv);
7797
+ forwardAwsEnv$3(dockerEnv);
7798
7798
  applyProfileCredentialsOverlay(dockerEnv, profileCredentials, false);
7799
7799
  if (profileCredsFile) {
7800
7800
  dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
@@ -7849,14 +7849,14 @@ async function localInvokeCommand(target, options, extraStateProviders) {
7849
7849
  }
7850
7850
  }
7851
7851
  async function resolveImagePlan(lambda, options) {
7852
- if (lambda.kind === "zip") return resolveZipImagePlan(lambda, options);
7853
- return resolveContainerImagePlan(lambda, options);
7852
+ if (lambda.kind === "zip") return resolveZipImagePlan$1(lambda, options);
7853
+ return resolveContainerImagePlan$1(lambda, options);
7854
7854
  }
7855
- async function resolveZipImagePlan(lambda, options) {
7855
+ async function resolveZipImagePlan$1(lambda, options) {
7856
7856
  let inlineTmpDir;
7857
7857
  let codeDir = lambda.codePath;
7858
7858
  if (codeDir === null) {
7859
- inlineTmpDir = materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
7859
+ inlineTmpDir = materializeInlineCode$2(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
7860
7860
  codeDir = inlineTmpDir;
7861
7861
  }
7862
7862
  const image = resolveRuntimeImage(lambda.runtime);
@@ -7933,7 +7933,7 @@ function materializeLambdaLayers$1(layers) {
7933
7933
  tmpDir
7934
7934
  };
7935
7935
  }
7936
- async function resolveContainerImagePlan(lambda, options) {
7936
+ async function resolveContainerImagePlan$1(lambda, options) {
7937
7937
  const logger = getLogger();
7938
7938
  const platform = architectureToPlatform(lambda.architecture);
7939
7939
  const localBuild = await resolveLocalBuildPlan$1(lambda);
@@ -8083,7 +8083,7 @@ async function assumeLambdaExecutionRole$1(roleArn, region) {
8083
8083
  sts.destroy();
8084
8084
  }
8085
8085
  }
8086
- function forwardAwsEnv$2(env) {
8086
+ function forwardAwsEnv$3(env) {
8087
8087
  for (const key of [
8088
8088
  "AWS_ACCESS_KEY_ID",
8089
8089
  "AWS_SECRET_ACCESS_KEY",
@@ -8123,7 +8123,7 @@ function applyProfileCredentialsOverlay(env, profileCreds, assumeRoleActive) {
8123
8123
  if (profileCreds.sessionToken) env["AWS_SESSION_TOKEN"] = profileCreds.sessionToken;
8124
8124
  else delete env["AWS_SESSION_TOKEN"];
8125
8125
  }
8126
- function materializeInlineCode$1(handler, source, fileExtension) {
8126
+ function materializeInlineCode$2(handler, source, fileExtension) {
8127
8127
  const lastDot = handler.lastIndexOf(".");
8128
8128
  if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
8129
8129
  const modulePath = handler.substring(0, lastDot);
@@ -12749,7 +12749,7 @@ const MOCK_API_ID = "local";
12749
12749
  * UTF-8; otherwise base64. Mirrors what API Gateway emits.
12750
12750
  */
12751
12751
  function buildHttpApiV2Event(req, ctx, opts = {}) {
12752
- const { rawPath, rawQueryString } = splitRawUrl$1(req.rawUrl);
12752
+ const { rawPath, rawQueryString } = splitRawUrl$2(req.rawUrl);
12753
12753
  const { headers, cookies } = normalizeHeadersV2(req.headers);
12754
12754
  const queryStringParameters = parseQueryStringV2(rawQueryString);
12755
12755
  const userAgent = headers["user-agent"] ?? "";
@@ -12809,7 +12809,7 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
12809
12809
  * - `pathParameters` may be `null` when there are none (matches AWS).
12810
12810
  */
12811
12811
  function buildRestV1Event(req, ctx, opts = {}) {
12812
- const { rawPath, rawQueryString } = splitRawUrl$1(req.rawUrl);
12812
+ const { rawPath, rawQueryString } = splitRawUrl$2(req.rawUrl);
12813
12813
  const { singular: headers, multi: multiValueHeaders } = normalizeHeadersV1(req.headers);
12814
12814
  const { singular: queryStringParameters, multi: multiValueQueryStringParameters } = parseQueryStringV1(rawQueryString);
12815
12815
  const contentType = headers["content-type"] ?? "";
@@ -12899,7 +12899,7 @@ function applyAuthorizerOverlay(event, overlay) {
12899
12899
  * `rawQueryString` (everything after, or `''`). Neither component is
12900
12900
  * decoded — that's the whole point of "raw" per the AWS spec.
12901
12901
  */
12902
- function splitRawUrl$1(rawUrl) {
12902
+ function splitRawUrl$2(rawUrl) {
12903
12903
  const q = rawUrl.indexOf("?");
12904
12904
  if (q === -1) return {
12905
12905
  rawPath: rawUrl,
@@ -13030,7 +13030,7 @@ function encodeBody(body, contentType) {
13030
13030
  body: "",
13031
13031
  isBase64Encoded: false
13032
13032
  };
13033
- if (isTextualContentType(contentType)) return {
13033
+ if (isTextualContentType$1(contentType)) return {
13034
13034
  body: body.toString("utf-8"),
13035
13035
  isBase64Encoded: false
13036
13036
  };
@@ -13051,7 +13051,7 @@ const TEXT_PREFIXES = [
13051
13051
  * Whether the given `Content-Type` value indicates textual data (so the
13052
13052
  * event body should be UTF-8 instead of base64).
13053
13053
  */
13054
- function isTextualContentType(contentType) {
13054
+ function isTextualContentType$1(contentType) {
13055
13055
  if (!contentType) return false;
13056
13056
  const lower = contentType.toLowerCase();
13057
13057
  return TEXT_PREFIXES.some((p) => lower.startsWith(p));
@@ -13116,7 +13116,7 @@ function formatRequestTime(d) {
13116
13116
  * v2 separates them.
13117
13117
  */
13118
13118
  function translateLambdaResponse(payload, version) {
13119
- if (isErrorEnvelope(payload)) return errorEnvelopeResponse();
13119
+ if (isErrorEnvelope$1(payload)) return errorEnvelopeResponse();
13120
13120
  if (isShapedResponse(payload)) return translateShapedResponse(payload, version);
13121
13121
  return autoFormatResponse(payload);
13122
13122
  }
@@ -13127,7 +13127,7 @@ function translateLambdaResponse(payload, version) {
13127
13127
  * matters because user code MAY return a Lambda Proxy response that
13128
13128
  * happens to have `errorMessage` as a payload field.
13129
13129
  */
13130
- function isErrorEnvelope(payload) {
13130
+ function isErrorEnvelope$1(payload) {
13131
13131
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
13132
13132
  const obj = payload;
13133
13133
  if ("statusCode" in obj) return false;
@@ -13185,7 +13185,7 @@ function translateShapedResponse(payload, version) {
13185
13185
  const headersIn = payload["headers"];
13186
13186
  if (headersIn && typeof headersIn === "object" && !Array.isArray(headersIn)) for (const [name, value] of Object.entries(headersIn)) {
13187
13187
  const lower = name.toLowerCase();
13188
- const stringValue = stringifyHeaderValue(value);
13188
+ const stringValue = stringifyHeaderValue$1(value);
13189
13189
  if (lower === "set-cookie") {
13190
13190
  cookies.push(stringValue);
13191
13191
  continue;
@@ -13197,7 +13197,7 @@ function translateShapedResponse(payload, version) {
13197
13197
  if (mvh && typeof mvh === "object" && !Array.isArray(mvh)) for (const [name, values] of Object.entries(mvh)) {
13198
13198
  if (!Array.isArray(values)) continue;
13199
13199
  const lower = name.toLowerCase();
13200
- const stringified = values.map((v) => stringifyHeaderValue(v));
13200
+ const stringified = values.map((v) => stringifyHeaderValue$1(v));
13201
13201
  if (lower === "set-cookie") {
13202
13202
  for (const c of stringified) cookies.push(c);
13203
13203
  continue;
@@ -13242,7 +13242,7 @@ function autoFormatResponse(payload) {
13242
13242
  * comma-joined to match the same dup-coalesce rule used on the request
13243
13243
  * side.
13244
13244
  */
13245
- function stringifyHeaderValue(value) {
13245
+ function stringifyHeaderValue$1(value) {
13246
13246
  if (value === null || value === void 0) return "";
13247
13247
  if (Array.isArray(value)) return value.map((v) => stringifyValue(v)).join(",");
13248
13248
  return stringifyValue(value);
@@ -14040,7 +14040,7 @@ function parseAuthorizationHeader(value) {
14040
14040
  * Signature = HexEncode(HMAC(SigningKey, StringToSign))
14041
14041
  */
14042
14042
  function computeSignature(req, parsed, secretAccessKey, amzDate) {
14043
- const { path, query } = splitRawUrl(req.rawUrl);
14043
+ const { path, query } = splitRawUrl$1(req.rawUrl);
14044
14044
  const canonicalUri = canonicalizePath(path);
14045
14045
  const canonicalQuery = canonicalizeQueryString(query);
14046
14046
  const headerLines = [];
@@ -14081,7 +14081,7 @@ function sha256Hex(buf) {
14081
14081
  * Important: keep the path RAW for canonicalization — the canonicalizer
14082
14082
  * does its own URI-encoding so we do NOT decode here.
14083
14083
  */
14084
- function splitRawUrl(rawUrl) {
14084
+ function splitRawUrl$1(rawUrl) {
14085
14085
  const q = rawUrl.indexOf("?");
14086
14086
  if (q < 0) return {
14087
14087
  path: rawUrl,
@@ -16669,7 +16669,7 @@ async function buildContainerSpec(args) {
16669
16669
  let imageRef;
16670
16670
  let platform;
16671
16671
  if (lambda.kind === "zip") {
16672
- codeDir = lambda.codePath ?? materializeInlineCode(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
16672
+ codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
16673
16673
  optDir = await materializeLambdaLayers(lambda.layers, layerTmpDirs, layerRoleArn);
16674
16674
  } else {
16675
16675
  imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile)).imageRef;
@@ -16729,7 +16729,7 @@ async function buildContainerSpec(args) {
16729
16729
  dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
16730
16730
  if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
16731
16731
  } else {
16732
- forwardAwsEnv$1(dockerEnv);
16732
+ forwardAwsEnv$2(dockerEnv);
16733
16733
  if (profileCredentials) {
16734
16734
  dockerEnv["AWS_ACCESS_KEY_ID"] = profileCredentials.accessKeyId;
16735
16735
  dockerEnv["AWS_SECRET_ACCESS_KEY"] = profileCredentials.secretAccessKey;
@@ -17071,7 +17071,7 @@ function formatRestV1IntegrationLabel(integration) {
17071
17071
  * them. (`cdkl invoke` runs once and `--rm` is the right model;
17072
17072
  * `cdkl start-api` lives across requests, so leaks compound.)
17073
17073
  */
17074
- function materializeInlineCode(handler, source, fileExtension, tmpDirsOut) {
17074
+ function materializeInlineCode$1(handler, source, fileExtension, tmpDirsOut) {
17075
17075
  const lastDot = handler.lastIndexOf(".");
17076
17076
  if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
17077
17077
  const modulePath = handler.substring(0, lastDot);
@@ -17113,7 +17113,7 @@ function readEnvOverridesFile$3(filePath) {
17113
17113
  * handler's AWS SDK calls can authenticate. Used when --assume-role is
17114
17114
  * NOT set for that Lambda — SAM-compatible default.
17115
17115
  */
17116
- function forwardAwsEnv$1(env) {
17116
+ function forwardAwsEnv$2(env) {
17117
17117
  for (const key of [
17118
17118
  "AWS_ACCESS_KEY_ID",
17119
17119
  "AWS_SECRET_ACCESS_KEY",
@@ -17893,7 +17893,7 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
17893
17893
  }
17894
17894
  }
17895
17895
  if (!assumeSucceeded) {
17896
- forwardAwsEnv(dockerEnv);
17896
+ forwardAwsEnv$1(dockerEnv);
17897
17897
  applyProfileCredentialsOverlay(dockerEnv, args.profileCredentials, false);
17898
17898
  if (args.profileCredsFile) {
17899
17899
  dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = args.profileCredsFile.containerPath;
@@ -17962,7 +17962,7 @@ function emitMcpResult(result) {
17962
17962
  function platformToArchitecture(platform) {
17963
17963
  return platform === "linux/amd64" ? "x86_64" : "arm64";
17964
17964
  }
17965
- function forwardAwsEnv(env) {
17965
+ function forwardAwsEnv$1(env) {
17966
17966
  for (const key of [
17967
17967
  "AWS_ACCESS_KEY_ID",
17968
17968
  "AWS_SECRET_ACCESS_KEY",
@@ -20606,6 +20606,203 @@ var FrontDoorEndpointPool = class {
20606
20606
  }
20607
20607
  };
20608
20608
 
20609
+ //#endregion
20610
+ //#region src/local/alb-lambda-event.ts
20611
+ /** Content types ALB treats as text (body sent verbatim, `isBase64Encoded: false`). */
20612
+ function isTextualContentType(contentType) {
20613
+ const ct = contentType.toLowerCase();
20614
+ return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/javascript") || ct.startsWith("application/xml");
20615
+ }
20616
+ /**
20617
+ * Build a `AlbHttpRequestSnapshot` from a live `node:http` request plus the
20618
+ * already-buffered body. Header values arrive as `string | string[]`; this
20619
+ * normalizes them to `string[]` (preserving multi-value) the builder expects.
20620
+ */
20621
+ function snapshotFromIncoming(req, body) {
20622
+ const headers = {};
20623
+ for (const [name, value] of Object.entries(req.headers)) {
20624
+ if (value === void 0) continue;
20625
+ headers[name] = Array.isArray(value) ? value : [value];
20626
+ }
20627
+ return {
20628
+ method: (req.method ?? "GET").toUpperCase(),
20629
+ rawUrl: req.url ?? "/",
20630
+ headers,
20631
+ body
20632
+ };
20633
+ }
20634
+ /** Split a raw URL into its path (query-stripped, not decoded) and raw query string. */
20635
+ function splitRawUrl(rawUrl) {
20636
+ const hashIdx = rawUrl.indexOf("#");
20637
+ const noHash = hashIdx === -1 ? rawUrl : rawUrl.slice(0, hashIdx);
20638
+ const qIdx = noHash.indexOf("?");
20639
+ if (qIdx === -1) return {
20640
+ path: noHash,
20641
+ rawQuery: ""
20642
+ };
20643
+ return {
20644
+ path: noHash.slice(0, qIdx),
20645
+ rawQuery: noHash.slice(qIdx + 1)
20646
+ };
20647
+ }
20648
+ /**
20649
+ * Parse a raw query string into single + multi-value maps. ALB does NOT decode
20650
+ * query parameters (the docs explicitly say "If the query parameters are
20651
+ * URL-encoded, the load balancer does not decode them"), so values are kept
20652
+ * verbatim. A bare `?flag` key yields the empty string value.
20653
+ */
20654
+ function parseQuery(rawQuery) {
20655
+ const single = {};
20656
+ const multi = {};
20657
+ if (rawQuery.length === 0) return {
20658
+ single,
20659
+ multi
20660
+ };
20661
+ for (const pair of rawQuery.split("&")) {
20662
+ if (pair.length === 0) continue;
20663
+ const eq = pair.indexOf("=");
20664
+ const key = eq === -1 ? pair : pair.slice(0, eq);
20665
+ const value = eq === -1 ? "" : pair.slice(eq + 1);
20666
+ single[key] = value;
20667
+ (multi[key] ??= []).push(value);
20668
+ }
20669
+ return {
20670
+ single,
20671
+ multi
20672
+ };
20673
+ }
20674
+ /**
20675
+ * Build the single + multi-value header maps. Names are lowercased; the single
20676
+ * map keeps the LAST value (ALB default-format), the multi map keeps every
20677
+ * value in arrival order.
20678
+ */
20679
+ function buildHeaderMaps(headers) {
20680
+ const single = {};
20681
+ const multi = {};
20682
+ for (const [name, values] of Object.entries(headers)) {
20683
+ const lower = name.toLowerCase();
20684
+ const list = values.slice();
20685
+ multi[lower] = list;
20686
+ if (list.length > 0) single[lower] = list[list.length - 1];
20687
+ }
20688
+ return {
20689
+ single,
20690
+ multi
20691
+ };
20692
+ }
20693
+ /** Header lookup that tolerates the case-folded multi-value request map. */
20694
+ function firstHeader(headers, name) {
20695
+ const lower = name.toLowerCase();
20696
+ for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === lower) return v[0];
20697
+ }
20698
+ /**
20699
+ * Build the ALB Lambda-target invocation event from an HTTP request snapshot.
20700
+ * Emits exactly the single-value OR multi-value variant per
20701
+ * `opts.multiValueHeaders`.
20702
+ */
20703
+ function buildAlbLambdaEvent(req, opts) {
20704
+ const { path, rawQuery } = splitRawUrl(req.rawUrl);
20705
+ const query = parseQuery(rawQuery);
20706
+ const headerMaps = buildHeaderMaps(req.headers);
20707
+ const contentEncoding = firstHeader(req.headers, "content-encoding");
20708
+ const contentType = firstHeader(req.headers, "content-type") ?? "";
20709
+ const isBase64Encoded = req.body.length > 0 && (contentEncoding !== void 0 ? true : !isTextualContentType(contentType));
20710
+ const body = isBase64Encoded ? req.body.toString("base64") : req.body.toString("utf-8");
20711
+ const event = {
20712
+ requestContext: { elb: { targetGroupArn: opts.targetGroupArn } },
20713
+ httpMethod: req.method,
20714
+ path,
20715
+ isBase64Encoded,
20716
+ body
20717
+ };
20718
+ if (opts.multiValueHeaders) {
20719
+ event["multiValueHeaders"] = headerMaps.multi;
20720
+ event["multiValueQueryStringParameters"] = query.multi;
20721
+ } else {
20722
+ event["headers"] = headerMaps.single;
20723
+ event["queryStringParameters"] = query.single;
20724
+ }
20725
+ return event;
20726
+ }
20727
+ /** A Lambda RIE error envelope (`{errorMessage, errorType, ...}`) with no statusCode. */
20728
+ function isErrorEnvelope(payload) {
20729
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
20730
+ const obj = payload;
20731
+ if ("statusCode" in obj) return false;
20732
+ return typeof obj["errorMessage"] === "string";
20733
+ }
20734
+ /**
20735
+ * The canonical 502 a real ALB returns when a Lambda target's response is
20736
+ * malformed (missing/invalid `statusCode`, non-object payload, or a runtime
20737
+ * error envelope). Plain-text body, mirroring ALB's own 502 page shape closely
20738
+ * enough for local dev.
20739
+ */
20740
+ function badGatewayResponse() {
20741
+ const body = Buffer.from("<html><body><h1>502 Bad Gateway</h1></body></html>", "utf-8");
20742
+ return {
20743
+ statusCode: 502,
20744
+ statusDescription: "502 Bad Gateway",
20745
+ headers: {
20746
+ "content-type": ["text/html"],
20747
+ "content-length": [String(body.length)]
20748
+ },
20749
+ body
20750
+ };
20751
+ }
20752
+ function stringifyHeaderValue(value) {
20753
+ if (value === null || value === void 0) return "";
20754
+ if (typeof value === "string") return value;
20755
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
20756
+ return JSON.stringify(value) ?? "";
20757
+ }
20758
+ /**
20759
+ * Translate a Lambda ALB-target response payload into HTTP components.
20760
+ *
20761
+ * A well-formed response is an object with a numeric `statusCode`. Headers come
20762
+ * from `headers` (single-value) and/or `multiValueHeaders` (array-valued); ALB
20763
+ * accepts either regardless of the request-side multi-value setting, so both
20764
+ * are honored here (multiValueHeaders extend / append to the single map).
20765
+ * `body` is optional; `isBase64Encoded: true` means the body is base64 and is
20766
+ * decoded to raw bytes.
20767
+ *
20768
+ * Anything else -> 502 (matching a real ALB), incl. a runtime error envelope.
20769
+ */
20770
+ function translateAlbLambdaResponse(payload) {
20771
+ if (isErrorEnvelope(payload)) return badGatewayResponse();
20772
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return badGatewayResponse();
20773
+ const obj = payload;
20774
+ const statusRaw = obj["statusCode"];
20775
+ if (typeof statusRaw !== "number" || !Number.isFinite(statusRaw)) return badGatewayResponse();
20776
+ const statusCode = Math.trunc(statusRaw);
20777
+ const isBase64 = obj["isBase64Encoded"] === true;
20778
+ const rawBody = obj["body"];
20779
+ let body;
20780
+ if (rawBody === void 0 || rawBody === null) body = Buffer.alloc(0);
20781
+ else if (typeof rawBody === "string") body = isBase64 ? Buffer.from(rawBody, "base64") : Buffer.from(rawBody, "utf-8");
20782
+ else body = Buffer.from(JSON.stringify(rawBody), "utf-8");
20783
+ const headers = {};
20784
+ const addHeader = (name, value) => {
20785
+ const lower = name.toLowerCase();
20786
+ (headers[lower] ??= []).push(value);
20787
+ };
20788
+ const singleHeaders = obj["headers"];
20789
+ if (singleHeaders && typeof singleHeaders === "object" && !Array.isArray(singleHeaders)) for (const [name, value] of Object.entries(singleHeaders)) addHeader(name, stringifyHeaderValue(value));
20790
+ const multiHeaders = obj["multiValueHeaders"];
20791
+ if (multiHeaders && typeof multiHeaders === "object" && !Array.isArray(multiHeaders)) for (const [name, values] of Object.entries(multiHeaders)) {
20792
+ if (!Array.isArray(values)) continue;
20793
+ for (const v of values) addHeader(name, stringifyHeaderValue(v));
20794
+ }
20795
+ headers["content-length"] = [String(body.length)];
20796
+ const result = {
20797
+ statusCode,
20798
+ headers,
20799
+ body
20800
+ };
20801
+ const statusDescription = obj["statusDescription"];
20802
+ if (typeof statusDescription === "string" && statusDescription.length > 0) result.statusDescription = statusDescription;
20803
+ return result;
20804
+ }
20805
+
20609
20806
  //#endregion
20610
20807
  //#region src/local/front-door-server.ts
20611
20808
  /** Default per-request upstream timeout — a hung replica yields a 504, not a hang. */
@@ -20646,31 +20843,56 @@ async function startFrontDoorServer(opts) {
20646
20843
  }
20647
20844
  };
20648
20845
  }
20846
+ /**
20847
+ * Resolve the dispatch target for a request path from whichever selector the
20848
+ * caller supplied. `selectTarget` (#123, ECS-or-Lambda) wins over the
20849
+ * pool-only `selectPool`; a `selectPool` hit is adapted to a `kind: 'pool'`
20850
+ * target so the request handler has a single code path.
20851
+ */
20852
+ function resolveDispatchTarget(opts, requestPath) {
20853
+ if (opts.selectTarget) return opts.selectTarget(requestPath);
20854
+ if (opts.selectPool) {
20855
+ const pool = opts.selectPool(requestPath);
20856
+ return pool ? {
20857
+ kind: "pool",
20858
+ pool
20859
+ } : void 0;
20860
+ }
20861
+ }
20649
20862
  function handleProxyRequest(req, res, opts) {
20650
- return new Promise((resolve) => {
20651
- const url = req.url ?? "/";
20863
+ const url = req.url ?? "/";
20864
+ if (opts.route) {
20652
20865
  const action = opts.route({
20653
20866
  path: url,
20654
20867
  ...hostHeader(req)
20655
20868
  });
20656
- if (!action) {
20657
- writeError(res, 404, `No listener rule matched '${url}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
20658
- resolve();
20659
- return;
20660
- }
20869
+ if (!action) return reply404(req, res, opts);
20661
20870
  if (action.kind === "redirect" || action.kind === "fixed-response") {
20662
20871
  req.resume();
20663
20872
  if (action.kind === "redirect") writeRedirect(res, action, req, opts.listenerPort);
20664
20873
  else writeFixedResponse(res, action);
20665
- resolve();
20666
- return;
20874
+ return Promise.resolve();
20667
20875
  }
20668
- const pool = pickWeightedPool(action.pools);
20669
- if (!pool) {
20876
+ const picked = pickWeightedTarget(action.pools);
20877
+ if (!picked) {
20670
20878
  writeError(res, 502, `No forward target selected behind ${opts.label} (every weighted target has weight 0).`);
20671
- resolve();
20672
- return;
20879
+ return Promise.resolve();
20673
20880
  }
20881
+ if ("lambda" in picked) return handleLambdaRequest(req, res, picked.lambda, opts);
20882
+ return handlePoolRequest(req, res, picked.pool, opts);
20883
+ }
20884
+ const target = resolveDispatchTarget(opts, url);
20885
+ if (!target) return reply404(req, res, opts);
20886
+ if (target.kind === "lambda") return handleLambdaRequest(req, res, target.lambda, opts);
20887
+ return handlePoolRequest(req, res, target.pool, opts);
20888
+ }
20889
+ /** Reply 404 — an ALB listener with no matching rule and no default action. */
20890
+ function reply404(req, res, opts) {
20891
+ writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
20892
+ return Promise.resolve();
20893
+ }
20894
+ function handlePoolRequest(req, res, pool, opts) {
20895
+ return new Promise((resolve) => {
20674
20896
  const endpoint = pool.next();
20675
20897
  if (!endpoint) {
20676
20898
  writeError(res, 503, `No running replicas behind ${opts.label} for the matched target. The front-door has no healthy target to forward to.`);
@@ -20727,23 +20949,24 @@ function hostHeader(req) {
20727
20949
  return host ? { host } : {};
20728
20950
  }
20729
20951
  /**
20730
- * Pick one pool from a weighted set: weighted random over the non-zero weights.
20731
- * A single-entry set short-circuits to that pool. Returns `undefined` when
20732
- * every weight is 0 (an ALB-valid but un-routable forward).
20952
+ * Pick one weighted target from a forward set: weighted random over the
20953
+ * non-zero weights. A single-entry set short-circuits to that entry. Returns
20954
+ * `undefined` when every weight is 0 (an ALB-valid but un-routable forward).
20955
+ * Used for forwards that may mix ECS pools and Lambda invokers.
20733
20956
  */
20734
- function pickWeightedPool(pools) {
20735
- if (pools.length === 0) return void 0;
20736
- if (pools.length === 1) return pools[0].weight > 0 ? pools[0].pool : void 0;
20737
- const total = pools.reduce((sum, p) => sum + Math.max(0, p.weight), 0);
20957
+ function pickWeightedTarget(targets) {
20958
+ if (targets.length === 0) return void 0;
20959
+ if (targets.length === 1) return targets[0].weight > 0 ? targets[0] : void 0;
20960
+ const total = targets.reduce((sum, t) => sum + Math.max(0, t.weight), 0);
20738
20961
  if (total <= 0) return void 0;
20739
20962
  let roll = Math.random() * total;
20740
- for (const p of pools) {
20741
- const w = Math.max(0, p.weight);
20963
+ for (const t of targets) {
20964
+ const w = Math.max(0, t.weight);
20742
20965
  if (w === 0) continue;
20743
20966
  roll -= w;
20744
- if (roll < 0) return p.pool;
20967
+ if (roll < 0) return t;
20745
20968
  }
20746
- for (let i = pools.length - 1; i >= 0; i--) if (Math.max(0, pools[i].weight) > 0) return pools[i].pool;
20969
+ for (let i = targets.length - 1; i >= 0; i--) if (Math.max(0, targets[i].weight) > 0) return targets[i];
20747
20970
  }
20748
20971
  /**
20749
20972
  * Synthesize an ALB-style redirect (301 / 302). ALB builds the `Location` from
@@ -20791,6 +21014,82 @@ function writeFixedResponse(res, action) {
20791
21014
  });
20792
21015
  res.end(body);
20793
21016
  }
21017
+ /** Maximum request body the ALB Lambda-target path buffers (ALB's own limit is 1 MB). */
21018
+ const ALB_LAMBDA_MAX_BODY_BYTES = 1024 * 1024;
21019
+ /**
21020
+ * Serve a request that resolved to a Lambda forward target (#123). Buffers the
21021
+ * request body (ALB caps the Lambda-target request body at 1 MB), translates
21022
+ * the request into the ALB Lambda-target event, invokes the function locally,
21023
+ * and writes the translated response. A malformed handler response or an
21024
+ * invoke failure surfaces as 502 — mirroring a real ALB.
21025
+ */
21026
+ function handleLambdaRequest(req, res, lambda, opts) {
21027
+ const logger = getLogger().child("front-door");
21028
+ return new Promise((resolve) => {
21029
+ let settled = false;
21030
+ const done = () => {
21031
+ if (settled) return;
21032
+ settled = true;
21033
+ resolve();
21034
+ };
21035
+ const chunks = [];
21036
+ let total = 0;
21037
+ let aborted = false;
21038
+ req.on("data", (chunk) => {
21039
+ if (aborted) return;
21040
+ total += chunk.length;
21041
+ if (total > ALB_LAMBDA_MAX_BODY_BYTES) {
21042
+ aborted = true;
21043
+ writeError(res, 413, `Request body exceeds the ${ALB_LAMBDA_MAX_BODY_BYTES}-byte Lambda-target limit on ${opts.label}.`);
21044
+ req.destroy();
21045
+ done();
21046
+ return;
21047
+ }
21048
+ chunks.push(chunk);
21049
+ });
21050
+ req.on("error", () => {
21051
+ if (!res.headersSent) writeError(res, 400, `Failed to read request body on ${opts.label}.`);
21052
+ done();
21053
+ });
21054
+ req.on("end", () => {
21055
+ if (aborted) return;
21056
+ const serveLambda = async () => {
21057
+ try {
21058
+ const body = Buffer.concat(chunks);
21059
+ const forwardHeaders = { ...req.headers };
21060
+ stripHopByHopHeaders(forwardHeaders);
21061
+ appendForwardedHeaders(forwardHeaders, req, opts.listenerPort);
21062
+ const snapshot = snapshotFromIncoming(req, body);
21063
+ for (const [name, value] of Object.entries(forwardHeaders)) {
21064
+ if (value === void 0) continue;
21065
+ snapshot.headers[name] = Array.isArray(value) ? value : [value];
21066
+ }
21067
+ const event = buildAlbLambdaEvent(snapshot, {
21068
+ targetGroupArn: lambda.targetGroupArn,
21069
+ multiValueHeaders: lambda.multiValueHeaders
21070
+ });
21071
+ const translated = translateAlbLambdaResponse(await lambda.invoke(event));
21072
+ if (res.headersSent || res.writableEnded) {
21073
+ done();
21074
+ return;
21075
+ }
21076
+ const outHeaders = {};
21077
+ for (const [name, values] of Object.entries(translated.headers)) outHeaders[name] = values.length === 1 ? values[0] : values;
21078
+ if (translated.statusDescription) res.writeHead(translated.statusCode, translated.statusDescription, outHeaders);
21079
+ else res.writeHead(translated.statusCode, outHeaders);
21080
+ res.end(translated.body);
21081
+ done();
21082
+ } catch (err) {
21083
+ logger.debug(`Lambda target '${lambda.label}' request failed: ${err instanceof Error ? err.message : String(err)}`);
21084
+ if (!res.headersSent) writeError(res, 502, `Lambda target '${lambda.label}' behind ${opts.label} failed.`);
21085
+ else if (!res.writableEnded) res.destroy();
21086
+ done();
21087
+ }
21088
+ };
21089
+ serveLambda();
21090
+ });
21091
+ });
21092
+ }
20794
21093
  /** Standard hop-by-hop headers (RFC 7230 §6.1) — a proxy must not forward these. */
20795
21094
  const HOP_BY_HOP_HEADERS = [
20796
21095
  "connection",
@@ -20915,6 +21214,186 @@ function globToRegExp(pattern, caseInsensitive) {
20915
21214
  return new RegExp(`^${body}$`);
20916
21215
  }
20917
21216
 
21217
+ //#endregion
21218
+ //#region src/local/front-door-lambda-runner.ts
21219
+ /** Forward the dev shell's AWS credential / region env into the Lambda container. */
21220
+ function forwardAwsEnv() {
21221
+ const env = {};
21222
+ for (const key of [
21223
+ "AWS_ACCESS_KEY_ID",
21224
+ "AWS_SECRET_ACCESS_KEY",
21225
+ "AWS_SESSION_TOKEN",
21226
+ "AWS_REGION",
21227
+ "AWS_DEFAULT_REGION"
21228
+ ]) {
21229
+ const value = process.env[key];
21230
+ if (value !== void 0) env[key] = value;
21231
+ }
21232
+ return env;
21233
+ }
21234
+ /**
21235
+ * Materialize an inline (`Code.ZipFile`) ZIP Lambda's source into a temp dir at
21236
+ * the path implied by `handler`, returning the dir to bind-mount. Mirrors
21237
+ * `local-invoke.ts:materializeInlineCode`.
21238
+ */
21239
+ function materializeInlineCode(handler, source, fileExtension) {
21240
+ const lastDot = handler.lastIndexOf(".");
21241
+ if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
21242
+ const modulePath = handler.substring(0, lastDot);
21243
+ const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
21244
+ const filePath = path.join(dir, `${modulePath}${fileExtension}`);
21245
+ mkdirSync(path.dirname(filePath), { recursive: true });
21246
+ writeFileSync(filePath, source, "utf-8");
21247
+ return dir;
21248
+ }
21249
+ async function resolveZipImagePlan(lambda, opts) {
21250
+ let inlineTmpDir;
21251
+ let codeDir = lambda.codePath;
21252
+ if (codeDir === null) {
21253
+ inlineTmpDir = materializeInlineCode(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
21254
+ codeDir = inlineTmpDir;
21255
+ }
21256
+ const image = resolveRuntimeImage(lambda.runtime);
21257
+ await pullImage(image, opts.skipPull === true);
21258
+ const containerCodePath = resolveRuntimeCodeMountPath(lambda.runtime);
21259
+ return {
21260
+ image,
21261
+ mounts: [{
21262
+ hostPath: codeDir,
21263
+ containerPath: containerCodePath,
21264
+ readOnly: true
21265
+ }],
21266
+ cmd: [lambda.handler],
21267
+ ...inlineTmpDir !== void 0 && { inlineTmpDir }
21268
+ };
21269
+ }
21270
+ async function resolveContainerImagePlan(lambda, opts) {
21271
+ const logger = getLogger().child("front-door-lambda");
21272
+ const platform = opts.platformOverride ?? architectureToPlatform(lambda.architecture);
21273
+ const manifestPath = lambda.stack.assetManifestPath;
21274
+ let imageRef;
21275
+ let localBuilt = false;
21276
+ if (manifestPath) {
21277
+ const cdkOutDir = path.dirname(manifestPath);
21278
+ const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
21279
+ const entry = manifest ? getDockerImageBySourceHash(manifest, lambda.imageUri) : void 0;
21280
+ if (entry) {
21281
+ imageRef = await buildContainerImage(entry.asset, cdkOutDir, { architecture: lambda.architecture });
21282
+ localBuilt = true;
21283
+ }
21284
+ }
21285
+ if (!localBuilt) {
21286
+ if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI ${getEmbedConfig().binaryName} can authenticate against. Re-synthesize the CDK app or deploy the image to ECR first.`);
21287
+ logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull...`);
21288
+ imageRef = await pullEcrImage(lambda.imageUri, {
21289
+ skipPull: opts.skipPull === true,
21290
+ ...opts.region !== void 0 && { region: opts.region },
21291
+ ...opts.ecrRoleArn !== void 0 && { ecrRoleArn: opts.ecrRoleArn }
21292
+ });
21293
+ }
21294
+ return {
21295
+ image: imageRef,
21296
+ mounts: [],
21297
+ cmd: lambda.imageConfig.command ?? [],
21298
+ platform,
21299
+ ...lambda.imageConfig.entryPoint && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
21300
+ ...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory }
21301
+ };
21302
+ }
21303
+ /**
21304
+ * Build a {@link FrontDoorLambdaRunner} for a resolved Lambda. Construction is
21305
+ * pure (no docker work); `start()` does the boot. The invoke timeout defaults
21306
+ * to `max(30s, timeoutSec * 2 * 1000)` — same formula as `cdkl invoke`.
21307
+ */
21308
+ function createFrontDoorLambdaRunner(lambda, opts) {
21309
+ const logger = getLogger().child("front-door-lambda");
21310
+ const defaultTimeoutMs = Math.max(3e4, lambda.timeoutSec * 2 * 1e3);
21311
+ let plan;
21312
+ let containerId;
21313
+ let hostPort;
21314
+ let stopLogStream;
21315
+ let starting;
21316
+ let stopped = false;
21317
+ async function doStart() {
21318
+ plan = lambda.kind === "zip" ? await resolveZipImagePlan(lambda, opts) : await resolveContainerImagePlan(lambda, opts);
21319
+ const port = await pickFreePort();
21320
+ hostPort = port;
21321
+ const name = `${getEmbedConfig().resourceNamePrefix}-alblambda-${lambda.logicalId}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
21322
+ const env = {
21323
+ AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
21324
+ AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
21325
+ AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
21326
+ AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
21327
+ AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${lambda.logicalId}`,
21328
+ AWS_LAMBDA_LOG_STREAM_NAME: "local",
21329
+ ...forwardAwsEnv()
21330
+ };
21331
+ logger.info(`Starting Lambda target container for ${lambda.logicalId} (image=${plan.image}, port=${port})...`);
21332
+ const id = await runDetached({
21333
+ image: plan.image,
21334
+ mounts: plan.mounts,
21335
+ env,
21336
+ cmd: plan.cmd,
21337
+ hostPort: port,
21338
+ host: opts.containerHost,
21339
+ name,
21340
+ ...plan.platform !== void 0 && { platform: plan.platform },
21341
+ ...plan.entryPoint !== void 0 && { entryPoint: plan.entryPoint },
21342
+ ...plan.workingDir !== void 0 && { workingDir: plan.workingDir }
21343
+ });
21344
+ containerId = id;
21345
+ stopLogStream = opts.streamLogs === false ? void 0 : streamLogs(id);
21346
+ try {
21347
+ await waitForRieReady(opts.containerHost, port, 3e4);
21348
+ } catch (err) {
21349
+ try {
21350
+ stopLogStream?.();
21351
+ } catch {}
21352
+ await removeContainer(id).catch(() => void 0);
21353
+ containerId = void 0;
21354
+ throw err;
21355
+ }
21356
+ }
21357
+ return {
21358
+ logicalId: lambda.logicalId,
21359
+ async start() {
21360
+ if (stopped) throw new Error("FrontDoorLambdaRunner.start called after stop");
21361
+ if (containerId) return;
21362
+ if (!starting) starting = doStart();
21363
+ await starting;
21364
+ },
21365
+ async invoke(event, timeoutMs) {
21366
+ if (!containerId || hostPort === void 0) throw new Error(`FrontDoorLambdaRunner('${lambda.logicalId}').invoke called before start() completed.`);
21367
+ return (await invokeRie(opts.containerHost, hostPort, event, timeoutMs ?? defaultTimeoutMs)).payload;
21368
+ },
21369
+ async stop() {
21370
+ if (stopped) return;
21371
+ stopped = true;
21372
+ try {
21373
+ stopLogStream?.();
21374
+ } catch (err) {
21375
+ logger.debug(`stopLogStream(${lambda.logicalId}) failed: ${err instanceof Error ? err.message : String(err)}`);
21376
+ }
21377
+ if (containerId) {
21378
+ try {
21379
+ await removeContainer(containerId);
21380
+ } catch (err) {
21381
+ logger.warn(`Failed to remove Lambda target container for ${lambda.logicalId}: ${err instanceof Error ? err.message : String(err)}. Continuing cleanup.`);
21382
+ }
21383
+ containerId = void 0;
21384
+ }
21385
+ if (plan?.inlineTmpDir) try {
21386
+ rmSync(plan.inlineTmpDir, {
21387
+ recursive: true,
21388
+ force: true
21389
+ });
21390
+ } catch (err) {
21391
+ logger.debug(`Failed to remove inline-code tmpdir ${plan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
21392
+ }
21393
+ }
21394
+ };
21395
+ }
21396
+
20918
21397
  //#endregion
20919
21398
  //#region src/cli/commands/ecs-service-emulator.ts
20920
21399
  /**
@@ -20935,6 +21414,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20935
21414
  let profileCredsFile;
20936
21415
  let frontDoorServers = [];
20937
21416
  let frontDoorByService = /* @__PURE__ */ new Map();
21417
+ let frontDoorLambdaRunners = [];
20938
21418
  const cleanup = singleFlight(async () => {
20939
21419
  await Promise.allSettled(perTarget.map(async (pt) => {
20940
21420
  if (pt.controller) await pt.controller.shutdown();
@@ -20945,6 +21425,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20945
21425
  }));
20946
21426
  await Promise.allSettled(frontDoorServers.map((s) => s.close().catch((err) => getLogger().warn(`front-door server teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
20947
21427
  frontDoorServers = [];
21428
+ await Promise.allSettled(frontDoorLambdaRunners.map((r) => r.stop().catch((err) => getLogger().warn(`front-door Lambda target teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
21429
+ frontDoorLambdaRunners = [];
20948
21430
  if (profileCredsFile) {
20949
21431
  try {
20950
21432
  await profileCredsFile.dispose();
@@ -20989,7 +21471,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20989
21471
  });
20990
21472
  const { boots, frontDoor, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
20991
21473
  for (const w of warnings) logger.warn(w);
20992
- if (boots.length === 0) throw new LocalStartServiceError(`No runnable ECS service resolved from ${resolvedTargets.join(", ")}.`);
21474
+ const hasFrontDoorListeners = !!frontDoor && frontDoor.listeners.length > 0;
21475
+ if (boots.length === 0 && !hasFrontDoorListeners) throw new LocalStartServiceError(`No runnable target resolved from ${resolvedTargets.join(", ")}.`);
20993
21476
  rejectExplicitCfnStackWithMultipleStacks(options, boots.length);
20994
21477
  perTarget = boots.map((boot) => ({
20995
21478
  boot,
@@ -21020,9 +21503,10 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
21020
21503
  sharedNetwork
21021
21504
  };
21022
21505
  if (frontDoor && frontDoor.listeners.length > 0) {
21023
- const built = await buildFrontDoor(frontDoor, options.containerHost, logger);
21506
+ const built = await buildFrontDoor(frontDoor, options, logger);
21024
21507
  frontDoorServers = built.servers;
21025
21508
  frontDoorByService = built.frontDoorByService;
21509
+ frontDoorLambdaRunners = built.lambdaRunners;
21026
21510
  }
21027
21511
  sigintHandler = () => {
21028
21512
  sigintCount += 1;
@@ -21036,10 +21520,13 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
21036
21520
  process.on("SIGINT", sigintHandler);
21037
21521
  process.on("SIGTERM", sigintHandler);
21038
21522
  for (const pt of perTarget) pt.controller = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService.get(pt.boot.target));
21039
- const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
21040
- logger.info(`Service(s) running: ${summary}.`);
21523
+ if (perTarget.length > 0) {
21524
+ const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
21525
+ logger.info(`Service(s) running: ${summary}.`);
21526
+ } else logger.info(`Service(s) running: ${frontDoorLambdaRunners.length} Lambda target(s) behind the ALB front-door.`);
21041
21527
  logger.info("Press ^C to shut down.");
21042
- await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
21528
+ if (perTarget.length > 0) await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
21529
+ else await new Promise(() => {});
21043
21530
  } finally {
21044
21531
  if (sigintHandler) {
21045
21532
  process.off("SIGINT", sigintHandler);
@@ -21137,28 +21624,63 @@ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull
21137
21624
  * already in use) every server started so far is closed and the error is
21138
21625
  * re-thrown with a `--lb-port` hint.
21139
21626
  */
21140
- async function buildFrontDoor(plan, containerHost, logger) {
21627
+ async function buildFrontDoor(plan, options, logger) {
21628
+ const containerHost = options.containerHost;
21141
21629
  const servers = [];
21142
- const registry = /* @__PURE__ */ new Map();
21143
- const poolFor = (t) => {
21630
+ const poolRegistry = /* @__PURE__ */ new Map();
21631
+ const lambdaRegistry = /* @__PURE__ */ new Map();
21632
+ const dispatchFor = (t) => {
21633
+ if (t.kind === "lambda") {
21634
+ let runner = lambdaRegistry.get(t.lambda.logicalId);
21635
+ if (!runner) {
21636
+ runner = createFrontDoorLambdaRunner(t.lambda, {
21637
+ containerHost,
21638
+ skipPull: options.pull === false,
21639
+ ...options.platform !== void 0 && { platformOverride: options.platform },
21640
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
21641
+ ...options.region !== void 0 && { region: options.region }
21642
+ });
21643
+ lambdaRegistry.set(t.lambda.logicalId, runner);
21644
+ }
21645
+ const boundRunner = runner;
21646
+ return {
21647
+ kind: "lambda",
21648
+ lambda: {
21649
+ targetGroupArn: t.targetGroupArn,
21650
+ multiValueHeaders: t.multiValueHeaders,
21651
+ label: t.lambda.logicalId,
21652
+ invoke: (event) => boundRunner.invoke(event)
21653
+ }
21654
+ };
21655
+ }
21144
21656
  const key = `${t.serviceTarget} ${t.targetContainerName} ${t.targetContainerPort}`;
21145
- let entry = registry.get(key);
21657
+ let entry = poolRegistry.get(key);
21146
21658
  if (!entry) {
21147
21659
  entry = {
21148
21660
  pool: new FrontDoorEndpointPool(),
21149
21661
  target: t
21150
21662
  };
21151
- registry.set(key, entry);
21663
+ poolRegistry.set(key, entry);
21152
21664
  }
21153
- return entry.pool;
21665
+ return {
21666
+ kind: "pool",
21667
+ pool: entry.pool
21668
+ };
21669
+ };
21670
+ const weightedTargetFor = (t) => {
21671
+ const dispatch = dispatchFor(t);
21672
+ return dispatch.kind === "lambda" ? {
21673
+ lambda: dispatch.lambda,
21674
+ weight: t.weight
21675
+ } : {
21676
+ pool: dispatch.pool,
21677
+ weight: t.weight
21678
+ };
21154
21679
  };
21155
21680
  const toRouteAction = (action) => {
21156
21681
  if (action.kind === "forward") return {
21157
21682
  kind: "forward",
21158
- pools: action.targets.map((t) => ({
21159
- pool: poolFor(t),
21160
- weight: t.weight
21161
- }))
21683
+ pools: action.targets.map(weightedTargetFor)
21162
21684
  };
21163
21685
  if (action.kind === "redirect") return {
21164
21686
  kind: "redirect",
@@ -21199,12 +21721,17 @@ async function buildFrontDoor(plan, containerHost, logger) {
21199
21721
  for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(` ${describeConditions(r)} (priority ${r.priority}) -> ${describeAction(r.action)}`);
21200
21722
  if (!listener.defaultAction) logger.info(" (no default action: unmatched requests return 404)");
21201
21723
  }
21724
+ for (const runner of lambdaRegistry.values()) {
21725
+ logger.info(`Booting Lambda target '${runner.logicalId}' behind the ALB front-door...`);
21726
+ await runner.start();
21727
+ }
21202
21728
  } catch (err) {
21203
21729
  await Promise.allSettled(servers.map((s) => s.close()));
21730
+ await Promise.allSettled([...lambdaRegistry.values()].map((r) => r.stop()));
21204
21731
  throw new LocalStartServiceError(`Failed to start ALB front-door: ${err instanceof Error ? err.message : String(err)}. If a listener port is privileged (< 1024), remap it to a non-privileged host port with --lb-port <listenerPort>=<hostPort> (e.g. --lb-port 80=8080).`);
21205
21732
  }
21206
21733
  const frontDoorByService = /* @__PURE__ */ new Map();
21207
- for (const { pool, target } of registry.values()) {
21734
+ for (const { pool, target } of poolRegistry.values()) {
21208
21735
  const list = frontDoorByService.get(target.serviceTarget) ?? [];
21209
21736
  list.push({
21210
21737
  pool,
@@ -21215,7 +21742,8 @@ async function buildFrontDoor(plan, containerHost, logger) {
21215
21742
  }
21216
21743
  return {
21217
21744
  servers,
21218
- frontDoorByService
21745
+ frontDoorByService,
21746
+ lambdaRunners: [...lambdaRegistry.values()]
21219
21747
  };
21220
21748
  }
21221
21749
  /** Human-readable summary of a planned rule's path / host conditions (for the boot banner). */
@@ -21229,11 +21757,17 @@ function describeConditions(rule) {
21229
21757
  function describeAction(action) {
21230
21758
  if (action.kind === "redirect") return `redirect ${action.statusCode}`;
21231
21759
  if (action.kind === "fixed-response") return `fixed-response ${action.statusCode}`;
21232
- if (action.targets.length === 1) {
21233
- const t = action.targets[0];
21234
- return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort}) (round-robin)`;
21235
- }
21236
- return `weighted forward [${action.targets.map((t) => `${t.serviceTarget}@${t.weight}`).join(", ")}]`;
21760
+ if (action.targets.length === 1) return describeTarget(action.targets[0]);
21761
+ return `weighted forward [${action.targets.map((t) => `${describeTargetShort(t)}@${t.weight}`).join(", ")}]`;
21762
+ }
21763
+ /** One forward target, described in full (for a single-target forward banner). */
21764
+ function describeTarget(t) {
21765
+ if (t.kind === "lambda") return `Lambda ${t.lambda.logicalId} (invoke)`;
21766
+ return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort}) (round-robin)`;
21767
+ }
21768
+ /** One forward target, described compactly (for a weighted-forward banner). */
21769
+ function describeTargetShort(t) {
21770
+ return t.kind === "lambda" ? `Lambda ${t.lambda.logicalId}` : t.serviceTarget;
21237
21771
  }
21238
21772
  async function resolvePlaceholderAccount(arn, region) {
21239
21773
  if (!arn.includes("${AWS::AccountId}")) return arn;
@@ -21468,17 +22002,20 @@ function createLocalStartServiceCommand(opts = {}) {
21468
22002
  * its path / host conditions.
21469
22003
  *
21470
22004
  * Scope: HTTP listeners; `path-pattern` + `host-header` conditions; `forward`
21471
- * (single or weighted) to ECS services; `redirect` / `fixed-response` actions.
21472
- * Skipped with a warning: HTTPS/TLS listeners, `TargetType:"lambda"` target
21473
- * groups, the other condition fields (http-header / http-request-method /
21474
- * query-string / source-ip), and `authenticate-cognito` / `authenticate-oidc`
21475
- * actions. Those remaining listener-rule features are tracked in #123.
22005
+ * (single or weighted) to ECS services AND/OR Lambda functions
22006
+ * (`TargetType:"lambda"` target groups — #123: the TG -> backing
22007
+ * `AWS::Lambda::Function` is resolved and the front-door invokes it locally per
22008
+ * request); `redirect` / `fixed-response` actions. A single weighted forward may
22009
+ * mix ECS and Lambda targets. Skipped with a warning: HTTPS/TLS listeners, the
22010
+ * other condition fields (http-header / http-request-method / query-string /
22011
+ * source-ip), and `authenticate-cognito` / `authenticate-oidc` actions.
21476
22012
  */
21477
22013
  const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
21478
22014
  const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
21479
22015
  const LISTENER_RULE_TYPE = "AWS::ElasticLoadBalancingV2::ListenerRule";
21480
22016
  const TARGET_GROUP_TYPE = "AWS::ElasticLoadBalancingV2::TargetGroup";
21481
22017
  const SERVICE_TYPE = "AWS::ECS::Service";
22018
+ const LAMBDA_FUNCTION_TYPE = "AWS::Lambda::Function";
21482
22019
  /**
21483
22020
  * Resolve an ALB into its front-door listeners + routing tables. Pure — reads
21484
22021
  * only the supplied stack template. Returns an empty `listeners` array (with
@@ -21572,7 +22109,12 @@ function resolveAction(actions, resources, tgToService, stackName, label, warnin
21572
22109
  }
21573
22110
  if (sawAuthenticate) warnings.push(`${label} is an authenticate-* action with no local-servable terminal action. The local ALB front-door does not enforce authenticate-cognito / authenticate-oidc; skipping it.`);
21574
22111
  }
21575
- /** Resolve a `forward` action into one or more weighted ECS-service targets. */
22112
+ /**
22113
+ * Resolve a `forward` action into one or more weighted targets. Each target
22114
+ * group is either an ECS service (the original `start-alb` path) or a
22115
+ * `TargetType: lambda` group backed by an in-stack Lambda (#123); a single
22116
+ * weighted forward may mix both.
22117
+ */
21576
22118
  function resolveForwardAction(action, resources, tgToService, stackName, label, warnings) {
21577
22119
  const refs = collectForwardTargetGroupRefs(action);
21578
22120
  if (refs.length === 0) {
@@ -21586,8 +22128,13 @@ function resolveForwardAction(action, resources, tgToService, stackName, label,
21586
22128
  warnings.push(`${label} forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stackName}. Skipping that target group.`);
21587
22129
  continue;
21588
22130
  }
21589
- if (tg.Properties?.["TargetType"] === "lambda") {
21590
- warnings.push(`${label} forwards to a Lambda target group '${tgRef}' (TargetType: lambda). The local ALB front-door supports ECS targets only; Lambda targets are deferred. Skipping that target group.`);
22131
+ const tgProps = tg.Properties ?? {};
22132
+ if (tgProps["TargetType"] === "lambda") {
22133
+ const lambdaTarget = resolveLambdaForwardTarget(tgProps, tgRef, resources, stackName, label, warnings);
22134
+ if (lambdaTarget) targets.push({
22135
+ ...lambdaTarget,
22136
+ weight
22137
+ });
21591
22138
  continue;
21592
22139
  }
21593
22140
  const backing = tgToService.get(tgRef);
@@ -21596,6 +22143,7 @@ function resolveForwardAction(action, resources, tgToService, stackName, label,
21596
22143
  continue;
21597
22144
  }
21598
22145
  targets.push({
22146
+ kind: "ecs",
21599
22147
  serviceLogicalId: backing.serviceLogicalId,
21600
22148
  targetContainerName: backing.containerName,
21601
22149
  targetContainerPort: backing.containerPort,
@@ -21609,6 +22157,29 @@ function resolveForwardAction(action, resources, tgToService, stackName, label,
21609
22157
  targets
21610
22158
  };
21611
22159
  }
22160
+ /**
22161
+ * Resolve a `TargetType: lambda` target group into its `FrontDoorLambdaTarget`
22162
+ * (weight applied by the caller), or `undefined` (with a warning) when the
22163
+ * backing function is not an in-stack `AWS::Lambda::Function` reference.
22164
+ */
22165
+ function resolveLambdaForwardTarget(tgProps, tgRef, resources, stackName, label, warnings) {
22166
+ const lambdaLogicalId = resolveLambdaTargetLogicalId(tgProps["Targets"]);
22167
+ if (!lambdaLogicalId) {
22168
+ warnings.push(`${label} forwards to a Lambda target group '${tgRef}', but its Targets[].Id is not an in-stack { "Fn::GetAtt": [<FnLogicalId>, "Arn"] } reference; the local ALB front-door supports an in-stack Lambda target only (literal / imported ARNs deferred). Skipping that target group.`);
22169
+ return;
22170
+ }
22171
+ const lambda = resources[lambdaLogicalId];
22172
+ if (!lambda || lambda.Type !== LAMBDA_FUNCTION_TYPE) {
22173
+ warnings.push(`${label} forwards to Lambda target group '${tgRef}', whose target resolves to '${lambdaLogicalId}', but no ${LAMBDA_FUNCTION_TYPE} with that logical id exists in ${stackName}. Skipping that target group.`);
22174
+ return;
22175
+ }
22176
+ return {
22177
+ kind: "lambda",
22178
+ lambdaLogicalId,
22179
+ targetGroupLogicalId: tgRef,
22180
+ multiValueHeaders: readMultiValueHeadersAttribute(tgProps["TargetGroupAttributes"])
22181
+ };
22182
+ }
21612
22183
  /** Resolve a `redirect` action into its `Location`-template fields + status code. */
21613
22184
  function resolveRedirectAction(action, label, warnings) {
21614
22185
  const cfg = action["RedirectConfig"];
@@ -21641,6 +22212,38 @@ function resolveFixedResponseAction(action) {
21641
22212
  return out;
21642
22213
  }
21643
22214
  /**
22215
+ * Resolve a `TargetType: lambda` target group's backing Lambda logical id from
22216
+ * its `Targets[].Id`. CDK synthesizes the registration as
22217
+ * `Targets: [{ Id: { "Fn::GetAtt": [<FnLogicalId>, "Arn"] } }]`; a `Ref` to the
22218
+ * function (its name) is also accepted. Returns the logical id, or `undefined`
22219
+ * when the target is a literal / imported ARN (not an in-stack reference).
22220
+ */
22221
+ function resolveLambdaTargetLogicalId(targets) {
22222
+ if (!Array.isArray(targets) || targets.length === 0) return void 0;
22223
+ const first = targets[0];
22224
+ if (!first || typeof first !== "object") return void 0;
22225
+ const id = first["Id"];
22226
+ if (!id || typeof id !== "object" || Array.isArray(id)) return void 0;
22227
+ const idObj = id;
22228
+ const getAtt = idObj["Fn::GetAtt"];
22229
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string" && getAtt[0].length > 0) return getAtt[0];
22230
+ const ref = idObj["Ref"];
22231
+ if (typeof ref === "string" && ref.length > 0) return ref;
22232
+ }
22233
+ /**
22234
+ * Read the `lambda.multi_value_headers.enabled` target-group attribute (a
22235
+ * string `"true"` / `"false"` in CFn). Defaults to `false` when absent.
22236
+ */
22237
+ function readMultiValueHeadersAttribute(attributes) {
22238
+ if (!Array.isArray(attributes)) return false;
22239
+ for (const attr of attributes) {
22240
+ if (!attr || typeof attr !== "object") continue;
22241
+ const a = attr;
22242
+ if (a["Key"] === "lambda.multi_value_headers.enabled") return String(a["Value"]).toLowerCase() === "true";
22243
+ }
22244
+ return false;
22245
+ }
22246
+ /**
21644
22247
  * Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
21645
22248
  * `AWS::ECS::Service.LoadBalancers[]`. First service wins on a shared target
21646
22249
  * group (unusual; would only happen with a hand-rolled template).
@@ -21900,19 +22503,28 @@ function albStrategy(options) {
21900
22503
  const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
21901
22504
  const resolution = resolveAlbFrontDoor(stack, albLogicalId);
21902
22505
  warnings.push(...resolution.warnings);
22506
+ const qualifyTarget = (t) => {
22507
+ if (t.kind === "lambda") return {
22508
+ kind: "lambda",
22509
+ lambda: resolveLambdaTarget(`${stack.stackName}:${t.lambdaLogicalId}`, stacks),
22510
+ targetGroupArn: `${stack.stackName}:${t.targetGroupLogicalId}`,
22511
+ multiValueHeaders: t.multiValueHeaders,
22512
+ weight: t.weight
22513
+ };
22514
+ const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
22515
+ serviceTargets.add(serviceTarget);
22516
+ return {
22517
+ kind: "ecs",
22518
+ serviceTarget,
22519
+ targetContainerName: t.targetContainerName,
22520
+ targetContainerPort: t.targetContainerPort,
22521
+ weight: t.weight
22522
+ };
22523
+ };
21903
22524
  const qualify = (action) => {
21904
22525
  if (action.kind === "forward") return {
21905
22526
  kind: "forward",
21906
- targets: action.targets.map((t) => {
21907
- const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
21908
- serviceTargets.add(serviceTarget);
21909
- return {
21910
- serviceTarget,
21911
- targetContainerName: t.targetContainerName,
21912
- targetContainerPort: t.targetContainerPort,
21913
- weight: t.weight
21914
- };
21915
- })
22527
+ targets: action.targets.map(qualifyTarget)
21916
22528
  };
21917
22529
  if (action.kind === "redirect") return {
21918
22530
  kind: "redirect",
@@ -21975,7 +22587,7 @@ function albStrategy(options) {
21975
22587
  */
21976
22588
  function createLocalStartAlbCommand(opts = {}) {
21977
22589
  setEmbedConfig(opts.embedConfig);
21978
- return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its HTTP listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP listeners; path-pattern and host-header rule conditions; forward (single and weighted), redirect, and fixed-response actions. HTTPS listeners, Lambda target groups, the other rule conditions (http-header / query-string / source-ip / http-request-method), and authenticate-* actions are skipped with a warning. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).action(withErrorHandling(async (targets, options) => {
22590
+ return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its HTTP listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP listeners; path-pattern and host-header rule conditions; forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). HTTPS listeners, the other rule conditions (http-header / query-string / source-ip / http-request-method), and authenticate-* actions are skipped with a warning. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).action(withErrorHandling(async (targets, options) => {
21979
22591
  await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
21980
22592
  })));
21981
22593
  }
@@ -22052,4 +22664,4 @@ function createLocalListCommand(opts = {}) {
22052
22664
 
22053
22665
  //#endregion
22054
22666
  export { createJwksCache as $, resolveLambdaArnIntrinsic as $t, invokeTokenAuthorizer as A, substituteAgainstStateAsync as At, VtlEvaluationError as B, resolveCfnRegion as Bt, resolveSelectionExpression as C, architectureToPlatform as Ct, computeRequestIdentityHash as D, resolveRuntimeImage as Dt, buildMethodArn as E, resolveRuntimeFileExtension as Et, buildRestV1Event as F, LocalStateSourceError as Ft, buildMgmtEndpointEnvUrl as G, resolveWatchConfig as Gt, probeHostGatewaySupport as H, CfnLocalStateProvider as Ht, evaluateResponseParameters as I, createLocalStateProvider as It, buildConnectEvent as J, discoverWebSocketApis as Jt, handleConnectionsRequest as K, countTargets as Kt, pickResponseTemplate as L, isCfnFlagPresent as Lt, translateLambdaResponse as M, substituteEnvVarsFromStateAsync as Mt, applyAuthorizerOverlay as N, resolveEnvVars as Nt, evaluateCachedLambdaPolicy as O, EcsTaskResolutionError as Ot, buildHttpApiV2Event as P, materializeLayerFromArn as Pt, buildJwksUrlFromIssuer as Q, pickRefLogicalId as Qt, selectIntegrationResponse as R, rejectExplicitCfnStackWithMultipleStacks as Rt, startApiServer as S, createLocalInvokeCommand as St, defaultCredentialsLoader as T, resolveRuntimeCodeMountPath as Tt, bufferToBody as U, collectSsmParameterRefs as Ut, HOST_GATEWAY_MIN_VERSION as V, resolveCfnStackName as Vt, ConnectionRegistry as W, resolveSsmParameters as Wt, buildMessageEvent as X, parseSelectionExpressionPath as Xt, buildDisconnectEvent as Y, discoverWebSocketApisOrThrow as Yt, buildCognitoJwksUrl as Z, discoverRoutes as Zt, availableApiIdentifiers as _, SUPPORTED_CODE_RUNTIMES as _t, buildCloudMapIndex as a, derivePseudoParametersFromRegion as an, buildCorsConfigByApiId as at, groupRoutesByServer as b, renderCodeDockerfile as bt, createLocalRunTaskCommand as c, LocalInvokeBuildError as cn, matchPreflight as ct, createWatchPredicates as d, MCP_PROTOCOL_VERSION as dt, AGENTCORE_HTTP_PROTOCOL as en, verifyCognitoJwt as et, resolveApiTargetSubset as f, mcpInvokeOnce as ft, buildStageMap as g, waitForAgentCorePing as gt, attachStageContext as h, invokeAgentCore as ht, createLocalStartServiceCommand as i, resolveAgentCoreTarget as in, applyCorsResponseHeaders as it, matchRoute as j, substituteEnvVarsFromState as jt, invokeRequestAuthorizer as k, substituteAgainstState as kt, createLocalInvokeAgentCoreCommand as l, MCP_CONTAINER_PORT as lt, createFileWatcher as m, AGENTCORE_SESSION_ID_HEADER as mt, formatTargetListing as n, AGENTCORE_RUNTIME_TYPE as nn, verifyJwtViaDiscovery as nt, CloudMapRegistry as o, substituteImagePlaceholders as on, buildCorsConfigFromCloudFrontChain as ot, createAuthorizerCache as p, parseSseForJsonRpc as pt, parseConnectionsPath as q, listTargets as qt, createLocalStartAlbCommand as r, AgentCoreResolutionError as rn, attachAuthorizers as rt, getContainerNetworkIp as s, tryResolveImageFnJoin as sn, isFunctionUrlOacFronted as st, createLocalListCommand as t, AGENTCORE_MCP_PROTOCOL as tn, verifyJwtAuthorizer as tt, createLocalStartApiCommand as u, MCP_PATH as ut, filterRoutesByApiIdentifier as v, buildAgentCoreCodeImage as vt, resolveServiceIntegrationParameters as w, buildContainerImage as wt, readMtlsMaterialsFromDisk as x, toCmdArgv as xt, filterRoutesByApiIdentifiers as y, computeCodeImageTag as yt, tryParseStatus as z, resolveCfnFallbackRegion as zt };
22055
- //# sourceMappingURL=local-list-EweOsy3A.js.map
22667
+ //# sourceMappingURL=local-list-jk2HzwoN.js.map