cdk-local 0.38.0 → 0.40.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.
@@ -3036,10 +3036,10 @@ function listTargets(stacks) {
3036
3036
  loadBalancers: sortEntries(scanApplicationLoadBalancers(stacks))
3037
3037
  };
3038
3038
  }
3039
- const pathOf = (e) => e.displayPath ?? e.qualifiedId;
3039
+ const pathOf$1 = (e) => e.displayPath ?? e.qualifiedId;
3040
3040
  /** Stable, human-readable ordering: by display path, falling back to the qualified ID. */
3041
3041
  function sortEntries(entries) {
3042
- return [...entries].sort((a, b) => pathOf(a).localeCompare(pathOf(b)));
3042
+ return [...entries].sort((a, b) => pathOf$1(a).localeCompare(pathOf$1(b)));
3043
3043
  }
3044
3044
  /** Display order for API surface kinds; entries are grouped in this order. */
3045
3045
  const API_KIND_ORDER = [
@@ -3060,7 +3060,7 @@ function sortApiEntries(entries) {
3060
3060
  const ka = API_KIND_ORDER.indexOf(a.kind ?? "");
3061
3061
  const kb = API_KIND_ORDER.indexOf(b.kind ?? "");
3062
3062
  if (ka !== kb) return ka - kb;
3063
- return pathOf(a).localeCompare(pathOf(b));
3063
+ return pathOf$1(a).localeCompare(pathOf$1(b));
3064
3064
  });
3065
3065
  }
3066
3066
  /** Total number of targets across every category. */
@@ -8232,10 +8232,12 @@ function isTransientNetworkError(err) {
8232
8232
  }
8233
8233
  /**
8234
8234
  * POST the event body to the agent's `/invocations` endpoint with the
8235
- * session-id header and a JSON content type. Returns the raw response body
8236
- * (JSON or SSE) together with its status + content type — the command
8237
- * prints it verbatim, so both the non-streaming JSON and streaming SSE
8238
- * shapes pass through unchanged.
8235
+ * session-id header and a JSON content type, then return the response.
8236
+ *
8237
+ * A `text/event-stream` response is streamed incrementally through
8238
+ * `options.onChunk` (when given) so the user sees tokens as they arrive — the
8239
+ * result is then `streamed: true` with an empty `raw`. Any other response (or
8240
+ * a missing sink) is buffered into `raw` and returned verbatim.
8239
8241
  */
8240
8242
  async function invokeAgentCore(host, port, event, options) {
8241
8243
  const url = `http://${host}:${port}${INVOCATIONS_PATH}`;
@@ -8254,11 +8256,22 @@ async function invokeAgentCore(host, port, event, options) {
8254
8256
  body,
8255
8257
  signal: controller.signal
8256
8258
  });
8259
+ const contentType = response.headers.get("content-type");
8260
+ if ((contentType ?? "").includes("text/event-stream") && options.onChunk && response.body) {
8261
+ await streamBody(response.body, options.onChunk);
8262
+ return {
8263
+ status: response.status,
8264
+ contentType,
8265
+ raw: "",
8266
+ streamed: true
8267
+ };
8268
+ }
8257
8269
  const raw = await response.text();
8258
8270
  return {
8259
8271
  status: response.status,
8260
- contentType: response.headers.get("content-type"),
8261
- raw
8272
+ contentType,
8273
+ raw,
8274
+ streamed: false
8262
8275
  };
8263
8276
  } catch (err) {
8264
8277
  if (err.name === "AbortError") throw new Error(`AgentCore invoke at ${url} timed out after ${options.timeoutMs}ms. The agent may be hung; check container logs.`);
@@ -8267,6 +8280,30 @@ async function invokeAgentCore(host, port, event, options) {
8267
8280
  clearTimeout(timer);
8268
8281
  }
8269
8282
  }
8283
+ /**
8284
+ * Decode a response body stream to UTF-8 text and push each chunk to `onChunk`
8285
+ * as it arrives. Uses the reader API (portable across Node versions) and a
8286
+ * streaming TextDecoder so a multi-byte char split across chunk boundaries is
8287
+ * not corrupted.
8288
+ */
8289
+ async function streamBody(body, onChunk) {
8290
+ const reader = body.getReader();
8291
+ const decoder = new TextDecoder();
8292
+ try {
8293
+ for (;;) {
8294
+ const { done, value } = await reader.read();
8295
+ if (done) break;
8296
+ if (value) {
8297
+ const text = decoder.decode(value, { stream: true });
8298
+ if (text) onChunk(text);
8299
+ }
8300
+ }
8301
+ const tail = decoder.decode();
8302
+ if (tail) onChunk(tail);
8303
+ } finally {
8304
+ reader.releaseLock();
8305
+ }
8306
+ }
8270
8307
 
8271
8308
  //#endregion
8272
8309
  //#region src/local/cors-handler.ts
@@ -17320,6 +17357,7 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
17320
17357
  const result = await invokeAgentCore(containerHost, hostPort, event, {
17321
17358
  sessionId,
17322
17359
  timeoutMs: 12e4,
17360
+ onChunk: (text) => process.stdout.write(text),
17323
17361
  ...authorization && { authorization }
17324
17362
  });
17325
17363
  await new Promise((r) => setTimeout(r, 250));
@@ -17496,6 +17534,10 @@ function emitResult(result) {
17496
17534
  logger.warn(`Agent /invocations returned HTTP ${result.status}.`);
17497
17535
  process.exitCode = 1;
17498
17536
  }
17537
+ if (result.streamed) {
17538
+ process.stdout.write("\n");
17539
+ return;
17540
+ }
17499
17541
  process.stdout.write(`${result.raw}\n`);
17500
17542
  }
17501
17543
  /** Map a `--platform` value to the architecture `buildContainerImage` expects. */
@@ -20188,9 +20230,15 @@ async function startFrontDoorServer(opts) {
20188
20230
  }
20189
20231
  function handleProxyRequest(req, res, opts) {
20190
20232
  return new Promise((resolve) => {
20191
- const endpoint = opts.pool.next();
20233
+ const pool = opts.selectPool(req.url ?? "/");
20234
+ if (!pool) {
20235
+ writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
20236
+ resolve();
20237
+ return;
20238
+ }
20239
+ const endpoint = pool.next();
20192
20240
  if (!endpoint) {
20193
- writeError(res, 503, `No running replicas for service '${opts.serviceName}'. The front-door has no healthy target to forward to.`);
20241
+ writeError(res, 503, `No running replicas behind ${opts.label} for the matched target. The front-door has no healthy target to forward to.`);
20194
20242
  resolve();
20195
20243
  return;
20196
20244
  }
@@ -20201,6 +20249,7 @@ function handleProxyRequest(req, res, opts) {
20201
20249
  resolve();
20202
20250
  };
20203
20251
  const headers = { ...req.headers };
20252
+ stripHopByHopHeaders(headers);
20204
20253
  appendForwardedHeaders(headers, req, opts.listenerPort);
20205
20254
  const proxyReq = request({
20206
20255
  host: endpoint.host,
@@ -20209,7 +20258,9 @@ function handleProxyRequest(req, res, opts) {
20209
20258
  path: req.url,
20210
20259
  headers
20211
20260
  }, (proxyRes) => {
20212
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
20261
+ const resHeaders = { ...proxyRes.headers };
20262
+ stripHopByHopHeaders(resHeaders);
20263
+ res.writeHead(proxyRes.statusCode ?? 502, resHeaders);
20213
20264
  proxyRes.pipe(res);
20214
20265
  proxyRes.on("end", done);
20215
20266
  proxyRes.on("error", () => {
@@ -20218,13 +20269,13 @@ function handleProxyRequest(req, res, opts) {
20218
20269
  });
20219
20270
  });
20220
20271
  proxyReq.setTimeout(opts.upstreamTimeoutMs ?? 3e4, () => {
20221
- if (!res.headersSent) writeError(res, 504, `Replica ${endpoint.host}:${endpoint.port} for service '${opts.serviceName}' did not respond in time.`);
20272
+ if (!res.headersSent) writeError(res, 504, `Replica ${endpoint.host}:${endpoint.port} behind ${opts.label} did not respond in time.`);
20222
20273
  else if (!res.writableEnded) res.destroy();
20223
20274
  proxyReq.destroy();
20224
20275
  done();
20225
20276
  });
20226
20277
  proxyReq.on("error", () => {
20227
- if (!res.headersSent) writeError(res, 502, `Failed to reach replica ${endpoint.host}:${endpoint.port} for service '${opts.serviceName}'.`);
20278
+ if (!res.headersSent) writeError(res, 502, `Failed to reach replica ${endpoint.host}:${endpoint.port} behind ${opts.label}.`);
20228
20279
  else if (!res.writableEnded) res.destroy();
20229
20280
  done();
20230
20281
  });
@@ -20234,6 +20285,33 @@ function handleProxyRequest(req, res, opts) {
20234
20285
  req.pipe(proxyReq);
20235
20286
  });
20236
20287
  }
20288
+ /** Standard hop-by-hop headers (RFC 7230 §6.1) — a proxy must not forward these. */
20289
+ const HOP_BY_HOP_HEADERS = [
20290
+ "connection",
20291
+ "keep-alive",
20292
+ "proxy-authenticate",
20293
+ "proxy-authorization",
20294
+ "te",
20295
+ "trailer",
20296
+ "transfer-encoding",
20297
+ "upgrade"
20298
+ ];
20299
+ /**
20300
+ * Strip hop-by-hop headers before relaying a request to / a response from the
20301
+ * upstream, mirroring what a real ALB does. Forwarding the upstream's
20302
+ * `Transfer-Encoding` / `Connection` verbatim while Node re-frames the body can
20303
+ * produce a malformed response; the headers named in a `Connection` token list
20304
+ * are also hop-by-hop and removed. Mutates `headers` in place.
20305
+ */
20306
+ function stripHopByHopHeaders(headers) {
20307
+ const connection = headers["connection"];
20308
+ const connectionValue = Array.isArray(connection) ? connection.join(",") : connection;
20309
+ if (connectionValue) for (const token of connectionValue.split(",")) {
20310
+ const name = token.trim().toLowerCase();
20311
+ if (name) delete headers[name];
20312
+ }
20313
+ for (const name of HOP_BY_HOP_HEADERS) delete headers[name];
20314
+ }
20237
20315
  /**
20238
20316
  * Inject the ALB-style forwarding headers a downstream app may read. Appends
20239
20317
  * the client IP to any existing `X-Forwarded-For` chain (ALB appends rather
@@ -20252,6 +20330,49 @@ function writeError(res, statusCode, message) {
20252
20330
  res.end(`${message}\n`);
20253
20331
  }
20254
20332
 
20333
+ //#endregion
20334
+ //#region src/local/alb-path-matcher.ts
20335
+ /**
20336
+ * Return the target of the highest-priority rule whose `path-pattern` matches
20337
+ * `requestPath`, or `undefined` when none match (caller uses the default).
20338
+ * Rules are evaluated in ascending priority; the input order is irrelevant.
20339
+ */
20340
+ function matchAlbPathRule(requestPath, rules) {
20341
+ const path = pathOf(requestPath);
20342
+ const ordered = [...rules].sort((a, b) => a.priority - b.priority);
20343
+ for (const rule of ordered) if (rule.pathPatterns.some((pattern) => albPathPatternMatches(pattern, path))) return rule.target;
20344
+ }
20345
+ /**
20346
+ * Whether a single ALB `path-pattern` value matches a request path. The path
20347
+ * must already be query-stripped, or pass a raw URL and it is stripped here.
20348
+ */
20349
+ function albPathPatternMatches(pattern, requestPath) {
20350
+ return globToRegExp(pattern).test(pathOf(requestPath));
20351
+ }
20352
+ /** Strip the query string / fragment so only the URL path is matched. */
20353
+ function pathOf(url) {
20354
+ let end = url.length;
20355
+ const q = url.indexOf("?");
20356
+ if (q !== -1) end = q;
20357
+ const h = url.indexOf("#");
20358
+ if (h !== -1 && h < end) end = h;
20359
+ return url.slice(0, end);
20360
+ }
20361
+ const REGEXP_META = /[.+^${}()|[\]\\]/;
20362
+ /**
20363
+ * Translate an ALB path-pattern glob into an anchored, case-sensitive RegExp:
20364
+ * `*` -> `.*`, `?` -> `.`, every other character is escaped and matched
20365
+ * literally.
20366
+ */
20367
+ function globToRegExp(pattern) {
20368
+ let body = "";
20369
+ for (const ch of pattern) if (ch === "*") body += ".*";
20370
+ else if (ch === "?") body += ".";
20371
+ else if (REGEXP_META.test(ch)) body += `\\${ch}`;
20372
+ else body += ch;
20373
+ return new RegExp(`^${body}$`);
20374
+ }
20375
+
20255
20376
  //#endregion
20256
20377
  //#region src/cli/commands/ecs-service-emulator.ts
20257
20378
  /**
@@ -20270,6 +20391,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20270
20391
  let sigintCount = 0;
20271
20392
  let sharedNetwork;
20272
20393
  let profileCredsFile;
20394
+ let frontDoorServers = [];
20395
+ let frontDoorByService = /* @__PURE__ */ new Map();
20273
20396
  const cleanup = singleFlight(async () => {
20274
20397
  await Promise.allSettled(perTarget.map(async (pt) => {
20275
20398
  if (pt.controller) await pt.controller.shutdown();
@@ -20277,8 +20400,9 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20277
20400
  await Promise.allSettled(pt.runState.replicas.map((r) => r.inFlightBoot).filter((p) => p !== void 0));
20278
20401
  await Promise.allSettled(pt.runState.replicas.map((r) => cleanupEcsRun(r.state, { keepRunning: false }).catch(() => void 0)));
20279
20402
  }
20280
- await Promise.allSettled((pt.frontDoorServers ?? []).map((s) => s.close().catch((err) => getLogger().warn(`front-door server teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
20281
20403
  }));
20404
+ await Promise.allSettled(frontDoorServers.map((s) => s.close().catch((err) => getLogger().warn(`front-door server teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
20405
+ frontDoorServers = [];
20282
20406
  if (profileCredsFile) {
20283
20407
  try {
20284
20408
  await profileCredsFile.dispose();
@@ -20321,7 +20445,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20321
20445
  noun: strategy.pickerNoun,
20322
20446
  onMissing: () => strategy.onMissing()
20323
20447
  });
20324
- const { boots, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
20448
+ const { boots, frontDoor, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
20325
20449
  for (const w of warnings) logger.warn(w);
20326
20450
  if (boots.length === 0) throw new LocalStartServiceError(`No runnable ECS service resolved from ${resolvedTargets.join(", ")}.`);
20327
20451
  rejectExplicitCfnStackWithMultipleStacks(options, boots.length);
@@ -20353,6 +20477,11 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20353
20477
  cloudMapIndexByStack,
20354
20478
  sharedNetwork
20355
20479
  };
20480
+ if (frontDoor && frontDoor.listeners.length > 0) {
20481
+ const built = await buildFrontDoor(frontDoor, options.containerHost, logger);
20482
+ frontDoorServers = built.servers;
20483
+ frontDoorByService = built.frontDoorByService;
20484
+ }
20356
20485
  sigintHandler = () => {
20357
20486
  sigintCount += 1;
20358
20487
  if (sigintCount >= 2) {
@@ -20364,11 +20493,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20364
20493
  };
20365
20494
  process.on("SIGINT", sigintHandler);
20366
20495
  process.on("SIGTERM", sigintHandler);
20367
- for (const pt of perTarget) {
20368
- const booted = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, strategy.lbPortOverrides);
20369
- pt.controller = booted.controller;
20370
- pt.frontDoorServers = booted.frontDoorServers;
20371
- }
20496
+ for (const pt of perTarget) pt.controller = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService.get(pt.boot.target));
20372
20497
  const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
20373
20498
  logger.info(`Service(s) running: ${summary}.`);
20374
20499
  logger.info("Press ^C to shut down.");
@@ -20381,16 +20506,16 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
20381
20506
  await cleanup();
20382
20507
  }
20383
20508
  }
20384
- async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, lbPortOverrides) {
20509
+ async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools) {
20385
20510
  const candidate = pickCandidateStack(parseEcsTarget(boot.target).stackPattern, stacks);
20386
20511
  const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
20387
20512
  try {
20388
- return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, lbPortOverrides);
20513
+ return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools);
20389
20514
  } finally {
20390
20515
  if (stateProvider) stateProvider.dispose();
20391
20516
  }
20392
20517
  }
20393
- async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, lbPortOverrides) {
20518
+ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools) {
20394
20519
  const logger = getLogger();
20395
20520
  const target = boot.target;
20396
20521
  const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
@@ -20446,73 +20571,89 @@ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull
20446
20571
  containerPath: profileCredsFile.containerPath,
20447
20572
  profileName: profileCredsFile.profileName
20448
20573
  };
20449
- const { frontDoorContext, frontDoorServers } = await startFrontDoorServers(boot.frontDoorTargets, service, options.containerHost, lbPortOverrides, logger);
20450
- const runnerOpts = {
20574
+ return startEcsService(service, {
20451
20575
  maxTasks: options.maxTasks,
20452
20576
  restartPolicy: options.restartPolicy,
20453
20577
  taskOptions: taskOpts,
20454
20578
  discovery,
20455
- ...frontDoorContext ? { frontDoor: frontDoorContext } : {}
20456
- };
20457
- let controller;
20458
- try {
20459
- controller = await startEcsService(service, runnerOpts, runState);
20460
- } catch (err) {
20461
- await Promise.allSettled(frontDoorServers.map((s) => s.close()));
20462
- throw err;
20463
- }
20464
- return {
20465
- controller,
20466
- frontDoorServers
20467
- };
20579
+ ...frontDoorPools && frontDoorPools.length > 0 ? { frontDoor: { pools: frontDoorPools } } : {}
20580
+ }, runState);
20468
20581
  }
20469
20582
  /**
20470
- * Issue #86 v1 — stand up one host-side reverse-proxy server per resolved
20471
- * front-door listener and return the {@link FrontDoorRunnerContext} to thread
20472
- * into the runner (so each replica publishes + registers its ephemeral
20473
- * endpoint), plus the started servers for teardown. Pure front-door MECHANISM:
20474
- * the ALB-specific resolution that produced `frontDoorTargets` lives in the
20475
- * `start-alb` command. Returns an undefined context + empty servers when there
20476
- * are no front-door targets (the pure-compute path).
20583
+ * Stand up one host-side reverse-proxy server PER LISTENER PORT from the
20584
+ * resolved {@link FrontDoorPlan}, path-routing each request across the services
20585
+ * the listener fronts, and return the started servers (for teardown) plus a
20586
+ * per-service-target pool list to thread into each service's runner (so every
20587
+ * replica publishes + registers its ephemeral endpoint into the right pool).
20588
+ *
20589
+ * One `FrontDoorEndpointPool` is created per distinct (service, container,
20590
+ * port) forward target and SHARED between the listener's routing table and the
20591
+ * owning service's runner context — same object on both sides, so a replica
20592
+ * registering itself is immediately reachable through the front-door.
20477
20593
  *
20478
20594
  * On a bind failure (e.g. EACCES on a privileged listener port, or the port is
20479
20595
  * already in use) every server started so far is closed and the error is
20480
20596
  * re-thrown with a `--lb-port` hint.
20481
20597
  */
20482
- async function startFrontDoorServers(frontDoorTargets, service, containerHost, lbPortOverrides, logger) {
20483
- if (frontDoorTargets.length === 0) return {
20484
- frontDoorContext: void 0,
20485
- frontDoorServers: []
20598
+ async function buildFrontDoor(plan, containerHost, logger) {
20599
+ const servers = [];
20600
+ const registry = /* @__PURE__ */ new Map();
20601
+ const poolFor = (t) => {
20602
+ const key = `${t.serviceTarget} ${t.targetContainerName} ${t.targetContainerPort}`;
20603
+ let entry = registry.get(key);
20604
+ if (!entry) {
20605
+ entry = {
20606
+ pool: new FrontDoorEndpointPool(),
20607
+ target: t
20608
+ };
20609
+ registry.set(key, entry);
20610
+ }
20611
+ return entry.pool;
20486
20612
  };
20487
- const frontDoorServers = [];
20488
- const pools = [];
20489
20613
  try {
20490
- for (const t of frontDoorTargets) {
20491
- const pool = new FrontDoorEndpointPool();
20614
+ for (const listener of plan.listeners) {
20615
+ const defaultPool = listener.defaultTarget ? poolFor(listener.defaultTarget) : void 0;
20616
+ const ruleRoutes = listener.rules.map((r) => ({
20617
+ priority: r.priority,
20618
+ pathPatterns: r.pathPatterns,
20619
+ target: poolFor(r.target)
20620
+ }));
20621
+ const selectPool = (requestPath) => matchAlbPathRule(requestPath, ruleRoutes) ?? defaultPool;
20492
20622
  const server = await startFrontDoorServer({
20493
- pool,
20494
- port: lbPortOverrides[t.listenerPort] ?? t.listenerPort,
20623
+ selectPool,
20624
+ port: listener.hostPort,
20495
20625
  host: containerHost,
20496
- listenerPort: t.listenerPort,
20497
- serviceName: service.serviceName
20626
+ listenerPort: listener.listenerPort,
20627
+ label: `listener port ${listener.listenerPort}`
20498
20628
  });
20499
- frontDoorServers.push(server);
20500
- pools.push({
20501
- pool,
20502
- targetContainerName: t.targetContainerName,
20503
- targetContainerPort: t.targetContainerPort
20504
- });
20505
- logger.info(`ALB front-door: http://${server.host}:${server.port} -> ${service.serviceName} (listener port ${t.listenerPort} -> container ${t.targetContainerName}:${t.targetContainerPort}, round-robin across replicas)`);
20629
+ servers.push(server);
20630
+ logger.info(`ALB front-door: http://${server.host}:${server.port} (listener port ${listener.listenerPort})`);
20631
+ if (listener.defaultTarget) logger.info(` default -> ${describeTarget(listener.defaultTarget)} (round-robin)`);
20632
+ for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(` path ${r.pathPatterns.join(", ")} (priority ${r.priority}) -> ${describeTarget(r.target)}`);
20633
+ if (!listener.defaultTarget) logger.info(" (no default action: unmatched paths return 404)");
20506
20634
  }
20507
20635
  } catch (err) {
20508
- await Promise.allSettled(frontDoorServers.map((s) => s.close()));
20509
- throw new LocalStartServiceError(`Failed to start ALB front-door for service '${service.serviceName}': ${err instanceof Error ? err.message : String(err)}. If the listener port is privileged (< 1024), remap it to a non-privileged host port with --lb-port <listenerPort>=<hostPort> (e.g. --lb-port 80=8080).`);
20636
+ await Promise.allSettled(servers.map((s) => s.close()));
20637
+ 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).`);
20638
+ }
20639
+ const frontDoorByService = /* @__PURE__ */ new Map();
20640
+ for (const { pool, target } of registry.values()) {
20641
+ const list = frontDoorByService.get(target.serviceTarget) ?? [];
20642
+ list.push({
20643
+ pool,
20644
+ targetContainerName: target.targetContainerName,
20645
+ targetContainerPort: target.targetContainerPort
20646
+ });
20647
+ frontDoorByService.set(target.serviceTarget, list);
20510
20648
  }
20511
20649
  return {
20512
- frontDoorContext: { pools },
20513
- frontDoorServers
20650
+ servers,
20651
+ frontDoorByService
20514
20652
  };
20515
20653
  }
20654
+ function describeTarget(t) {
20655
+ return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort})`;
20656
+ }
20516
20657
  async function resolvePlaceholderAccount(arn, region) {
20517
20658
  if (!arn.includes("${AWS::AccountId}")) return arn;
20518
20659
  const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
@@ -20687,10 +20828,7 @@ function serviceStrategy() {
20687
20828
  pickerNoun: "ECS services",
20688
20829
  onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend', or run it in a TTY to pick interactively.`),
20689
20830
  resolveBoots: (_stacks, chosenTargets) => ({
20690
- boots: chosenTargets.map((target) => ({
20691
- target,
20692
- frontDoorTargets: []
20693
- })),
20831
+ boots: chosenTargets.map((target) => ({ target })),
20694
20832
  warnings: []
20695
20833
  }),
20696
20834
  lbPortOverrides: {}
@@ -20713,47 +20851,57 @@ function createLocalStartServiceCommand(opts = {}) {
20713
20851
  //#endregion
20714
20852
  //#region src/local/elb-front-door-resolver.ts
20715
20853
  /**
20716
- * Issue #86 v1 — resolve an `AWS::ElasticLoadBalancingV2::LoadBalancer` (an
20717
- * ALB) into the backing ECS service(s) and the host listener port(s) a local
20718
- * front-door should expose for each. This is the `cdkl start-alb` entry: you
20719
- * name the ALB, and cdk-local discovers the services behind it (mirroring how
20720
- * `start-api` names the API and discovers the backing Lambdas).
20854
+ * Resolve an `AWS::ElasticLoadBalancingV2::LoadBalancer` (an ALB) into the
20855
+ * backing ECS service(s) and the host listener port(s) a local front-door
20856
+ * should expose. This is the `cdkl start-alb` entry: you name the ALB, and
20857
+ * cdk-local discovers the services behind it (mirroring how `start-api` names
20858
+ * the API and discovers the backing Lambdas).
20721
20859
  *
20722
20860
  * The synthesized linkage (confirmed against real `cdk synth` of
20723
- * `ApplicationLoadBalancedFargateService`):
20861
+ * `ApplicationLoadBalancedFargateService` + an `addAction` path rule):
20724
20862
  *
20725
20863
  * ```
20726
20864
  * ElasticLoadBalancingV2::LoadBalancer (the ALB you name)
20727
20865
  * ElasticLoadBalancingV2::Listener : { LoadBalancerArn:{Ref:<ALB>}, Port, Protocol,
20728
20866
  * DefaultActions:[{ Type:"forward", TargetGroupArn:{Ref:<TG>} }] }
20867
+ * ElasticLoadBalancingV2::ListenerRule : { ListenerArn:{Ref:<Listener>}, Priority,
20868
+ * Conditions:[{ Field:"path-pattern", PathPatternConfig:{ Values:["/api/*"] } }],
20869
+ * Actions:[{ Type:"forward", TargetGroupArn:{Ref:<TG>} }] }
20729
20870
  * ElasticLoadBalancingV2::TargetGroup : { Port, Protocol, TargetType:"ip" }
20730
20871
  * ECS::Service.LoadBalancers[] -> { ContainerName, ContainerPort, TargetGroupArn:{Ref:<TG>} }
20731
20872
  * ```
20732
20873
  *
20733
- * Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> default
20734
- * `forward` target groups -> the ECS Service whose `LoadBalancers[]` references
20735
- * that target group (a reverse scan; there is no direct TG -> service pointer).
20874
+ * Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> their default
20875
+ * `forward` action AND any `path-pattern` ListenerRules -> the ECS Service whose
20876
+ * `LoadBalancers[]` references each target group (a reverse scan; there is no
20877
+ * direct TG -> service pointer). Output is a per-listener routing table: a
20878
+ * default forward target (when the default action is a resolvable forward) plus
20879
+ * the ordered path-pattern rules.
20736
20880
  *
20737
- * v1 scope (single forward): only listener `DefaultActions` are honored.
20738
- * `AWS::ElasticLoadBalancingV2::ListenerRule` (path / host / weighted routing)
20739
- * is ignored — tracked in #123. HTTPS / TLS listeners and `TargetType:"lambda"`
20740
- * target groups are skipped with a warning.
20881
+ * Scope: HTTP listeners, `path-pattern` conditions, single-target `forward`
20882
+ * actions to ECS services. Skipped with a warning: HTTPS/TLS listeners,
20883
+ * `TargetType:"lambda"` target groups, weighted (multi-target) forwards, rules
20884
+ * with non-`path-pattern` conditions (host-header / http-header / query-string
20885
+ * / etc.), and `redirect` / `fixed-response` / `authenticate-*` actions. Those
20886
+ * remaining listener-rule features are tracked in #123.
20741
20887
  */
20742
20888
  const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
20743
20889
  const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
20890
+ const LISTENER_RULE_TYPE = "AWS::ElasticLoadBalancingV2::ListenerRule";
20744
20891
  const TARGET_GROUP_TYPE = "AWS::ElasticLoadBalancingV2::TargetGroup";
20745
20892
  const SERVICE_TYPE = "AWS::ECS::Service";
20746
20893
  /**
20747
- * Resolve an ALB into its backing services + front-door targets. Pure — reads
20748
- * only the supplied stack template. Returns an empty `services` array (with
20894
+ * Resolve an ALB into its front-door listeners + routing tables. Pure — reads
20895
+ * only the supplied stack template. Returns an empty `listeners` array (with
20749
20896
  * warnings) when the ALB fronts nothing cdk-local can serve locally.
20750
20897
  */
20751
20898
  function resolveAlbFrontDoor(stack, albLogicalId) {
20752
20899
  const warnings = [];
20753
20900
  const resources = stack.template.Resources ?? {};
20901
+ const stackName = stack.stackName;
20754
20902
  const tgToService = indexTargetGroupToService(resources);
20755
- const byService = /* @__PURE__ */ new Map();
20756
- const seenPortsByService = /* @__PURE__ */ new Map();
20903
+ const rulesByListener = indexRulesByListener(resources);
20904
+ const listeners = [];
20757
20905
  for (const [listenerLogicalId, resource] of Object.entries(resources)) {
20758
20906
  if (resource.Type !== LISTENER_TYPE) continue;
20759
20907
  const props = resource.Properties ?? {};
@@ -20761,54 +20909,40 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
20761
20909
  const port = parsePort(props["Port"]);
20762
20910
  if (port === void 0) continue;
20763
20911
  const protocol = typeof props["Protocol"] === "string" ? props["Protocol"] : "HTTP";
20764
- const tgRefs = collectForwardTargetGroupRefs(props["DefaultActions"]);
20765
- if (tgRefs.size === 0) {
20766
- if (hasUnresolvableForward(props["DefaultActions"])) warnings.push(`Listener '${listenerLogicalId}' on port ${port} forwards to a non-Ref TargetGroupArn (literal / cross-stack / imported); the local front-door only supports in-stack target groups. Skipping it.`);
20767
- continue;
20768
- }
20769
20912
  if (protocol !== "HTTP") {
20770
- warnings.push(`Listener '${listenerLogicalId}' on port ${port} uses protocol ${protocol}; the local ALB front-door supports HTTP listeners only in v1 (TLS termination is deferred). Skipping it.`);
20913
+ warnings.push(`Listener '${listenerLogicalId}' on port ${port} uses protocol ${protocol}; the local ALB front-door supports HTTP listeners only (TLS termination is deferred). Skipping it.`);
20771
20914
  continue;
20772
20915
  }
20773
- for (const tgRef of tgRefs) {
20774
- const tg = resources[tgRef];
20775
- if (!tg || tg.Type !== TARGET_GROUP_TYPE) {
20776
- warnings.push(`Listener '${listenerLogicalId}' forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stack.stackName}. Skipping it.`);
20777
- continue;
20778
- }
20779
- if (tg.Properties?.["TargetType"] === "lambda") {
20780
- warnings.push(`Target group '${tgRef}' is a Lambda target (TargetType: lambda). The local ALB front-door supports ECS targets only in v1; Lambda targets are deferred to a follow-up. Skipping it.`);
20781
- continue;
20782
- }
20783
- const backing = tgToService.get(tgRef);
20784
- if (!backing) {
20785
- warnings.push(`Target group '${tgRef}' (listener '${listenerLogicalId}', port ${port}) is not referenced by any ${SERVICE_TYPE}.LoadBalancers[] in ${stack.stackName}; cdk-local has no ECS service to front behind it. Skipping it.`);
20916
+ const defaultTarget = resolveForwardTarget(props["DefaultActions"], resources, tgToService, stackName, `Listener '${listenerLogicalId}' (port ${port}) default action`, warnings);
20917
+ const rules = [];
20918
+ for (const { ruleLogicalId, ruleProps } of rulesByListener.get(listenerLogicalId) ?? []) {
20919
+ const priority = parsePriority(ruleProps["Priority"]);
20920
+ const ruleLabel = `Listener rule '${ruleLogicalId}' (priority ${priority})`;
20921
+ const { patterns, unsupported } = parseRulePathPatterns(ruleProps["Conditions"]);
20922
+ if (unsupported.length > 0) {
20923
+ warnings.push(`${ruleLabel} uses unsupported condition(s): ${unsupported.join(", ")}. The local ALB front-door supports path-pattern conditions only (host-header / http-header / query-string / http-request-method / source-ip deferred). Skipping it.`);
20786
20924
  continue;
20787
20925
  }
20788
- const seenPorts = seenPortsByService.get(backing.serviceLogicalId) ?? /* @__PURE__ */ new Set();
20789
- if (seenPorts.has(port)) {
20790
- warnings.push(`Service '${backing.serviceLogicalId}' is fronted by more than one listener on host port ${port}; the local front-door fronts only the first.`);
20791
- continue;
20792
- }
20793
- seenPorts.add(port);
20794
- seenPortsByService.set(backing.serviceLogicalId, seenPorts);
20795
- const targets = byService.get(backing.serviceLogicalId) ?? [];
20796
- targets.push({
20797
- listenerPort: port,
20798
- listenerProtocol: "HTTP",
20799
- targetContainerName: backing.containerName,
20800
- targetContainerPort: backing.containerPort,
20801
- targetGroupLogicalId: tgRef,
20802
- listenerLogicalId
20926
+ if (patterns.length === 0) continue;
20927
+ const target = resolveForwardTarget(ruleProps["Actions"], resources, tgToService, stackName, `${ruleLabel} action`, warnings);
20928
+ if (!target) continue;
20929
+ rules.push({
20930
+ priority,
20931
+ pathPatterns: patterns,
20932
+ target
20803
20933
  });
20804
- byService.set(backing.serviceLogicalId, targets);
20805
20934
  }
20935
+ if (!defaultTarget && rules.length === 0) continue;
20936
+ listeners.push({
20937
+ listenerPort: port,
20938
+ listenerProtocol: "HTTP",
20939
+ listenerLogicalId,
20940
+ ...defaultTarget ? { defaultTarget } : {},
20941
+ rules
20942
+ });
20806
20943
  }
20807
20944
  return {
20808
- services: [...byService.entries()].map(([serviceLogicalId, targets]) => ({
20809
- serviceLogicalId,
20810
- targets
20811
- })),
20945
+ listeners,
20812
20946
  warnings
20813
20947
  };
20814
20948
  }
@@ -20819,6 +20953,43 @@ function isApplicationLoadBalancer(resource) {
20819
20953
  return type === void 0 || type === "application";
20820
20954
  }
20821
20955
  /**
20956
+ * Resolve a listener / rule `Actions` (or `DefaultActions`) array to a single
20957
+ * ECS-service forward target, or `undefined` when it is not a resolvable
20958
+ * single forward (warning emitted for the cases worth surfacing).
20959
+ */
20960
+ function resolveForwardTarget(actions, resources, tgToService, stackName, label, warnings) {
20961
+ const tgRefs = collectForwardTargetGroupRefs(actions);
20962
+ if (tgRefs.size === 0) {
20963
+ if (hasUnresolvableForward(actions)) warnings.push(`${label} forwards to a non-Ref TargetGroupArn (literal / cross-stack / imported); the local front-door only supports in-stack target groups. Skipping it.`);
20964
+ return;
20965
+ }
20966
+ if (tgRefs.size > 1) {
20967
+ warnings.push(`${label} is a weighted forward (multiple target groups); the local front-door supports a single target group per action (weighted routing deferred). Skipping it.`);
20968
+ return;
20969
+ }
20970
+ const tgRef = [...tgRefs][0];
20971
+ const tg = resources[tgRef];
20972
+ if (!tg || tg.Type !== TARGET_GROUP_TYPE) {
20973
+ warnings.push(`${label} forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stackName}. Skipping it.`);
20974
+ return;
20975
+ }
20976
+ if (tg.Properties?.["TargetType"] === "lambda") {
20977
+ 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 it.`);
20978
+ return;
20979
+ }
20980
+ const backing = tgToService.get(tgRef);
20981
+ if (!backing) {
20982
+ warnings.push(`${label} forwards to target group '${tgRef}', which is not referenced by any ${SERVICE_TYPE}.LoadBalancers[] in ${stackName}; cdk-local has no ECS service to front behind it. Skipping it.`);
20983
+ return;
20984
+ }
20985
+ return {
20986
+ serviceLogicalId: backing.serviceLogicalId,
20987
+ targetContainerName: backing.containerName,
20988
+ targetContainerPort: backing.containerPort,
20989
+ targetGroupLogicalId: tgRef
20990
+ };
20991
+ }
20992
+ /**
20822
20993
  * Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
20823
20994
  * `AWS::ECS::Service.LoadBalancers[]`. First service wins on a shared target
20824
20995
  * group (unusual; would only happen with a hand-rolled template).
@@ -20845,10 +21016,58 @@ function indexTargetGroupToService(resources) {
20845
21016
  }
20846
21017
  return index;
20847
21018
  }
20848
- function collectForwardTargetGroupRefs(defaultActions) {
21019
+ /** Group every `AWS::ElasticLoadBalancingV2::ListenerRule` by the listener it references. */
21020
+ function indexRulesByListener(resources) {
21021
+ const index = /* @__PURE__ */ new Map();
21022
+ for (const [ruleLogicalId, resource] of Object.entries(resources)) {
21023
+ if (resource.Type !== LISTENER_RULE_TYPE) continue;
21024
+ const ruleProps = resource.Properties ?? {};
21025
+ const listenerRef = refOf(ruleProps["ListenerArn"]);
21026
+ if (!listenerRef) continue;
21027
+ const list = index.get(listenerRef) ?? [];
21028
+ list.push({
21029
+ ruleLogicalId,
21030
+ ruleProps
21031
+ });
21032
+ index.set(listenerRef, list);
21033
+ }
21034
+ return index;
21035
+ }
21036
+ /**
21037
+ * Parse a ListenerRule's `Conditions` into its `path-pattern` values plus the
21038
+ * field names of any non-`path-pattern` conditions (which make the rule
21039
+ * unsupported in this version). A rule is only usable when every condition is a
21040
+ * `path-pattern` — ALB ANDs conditions together, and we cannot honor the others
21041
+ * locally yet.
21042
+ */
21043
+ function parseRulePathPatterns(conditions) {
21044
+ const patterns = [];
21045
+ const unsupported = [];
21046
+ if (!Array.isArray(conditions)) return {
21047
+ patterns,
21048
+ unsupported
21049
+ };
21050
+ for (const cond of conditions) {
21051
+ if (!cond || typeof cond !== "object") continue;
21052
+ const c = cond;
21053
+ const field = typeof c["Field"] === "string" ? c["Field"] : "(unknown)";
21054
+ if (field !== "path-pattern") {
21055
+ unsupported.push(field);
21056
+ continue;
21057
+ }
21058
+ const cfg = c["PathPatternConfig"];
21059
+ const values = cfg && typeof cfg === "object" && Array.isArray(cfg["Values"]) ? cfg["Values"] : Array.isArray(c["Values"]) ? c["Values"] : [];
21060
+ for (const v of values) if (typeof v === "string") patterns.push(v);
21061
+ }
21062
+ return {
21063
+ patterns,
21064
+ unsupported
21065
+ };
21066
+ }
21067
+ function collectForwardTargetGroupRefs(actions) {
20849
21068
  const refs = /* @__PURE__ */ new Set();
20850
- if (!Array.isArray(defaultActions)) return refs;
20851
- for (const action of defaultActions) {
21069
+ if (!Array.isArray(actions)) return refs;
21070
+ for (const action of actions) {
20852
21071
  if (!action || typeof action !== "object") continue;
20853
21072
  const a = action;
20854
21073
  if (a["Type"] !== "forward") continue;
@@ -20867,14 +21086,14 @@ function collectForwardTargetGroupRefs(defaultActions) {
20867
21086
  return refs;
20868
21087
  }
20869
21088
  /**
20870
- * True when `DefaultActions` has at least one `forward` action that references
20871
- * a target group via a NON-`Ref` arn (literal / `Fn::GetAtt` / cross-stack) —
20872
- * i.e. a forward we could not resolve to an in-stack target group. Used to warn
20873
- * rather than silently skip such a listener.
21089
+ * True when `actions` has at least one `forward` action that references a target
21090
+ * group via a NON-`Ref` arn (literal / `Fn::GetAtt` / cross-stack) — i.e. a
21091
+ * forward we could not resolve to an in-stack target group. Used to warn rather
21092
+ * than silently skip such a listener / rule.
20874
21093
  */
20875
- function hasUnresolvableForward(defaultActions) {
20876
- if (!Array.isArray(defaultActions)) return false;
20877
- for (const action of defaultActions) {
21094
+ function hasUnresolvableForward(actions) {
21095
+ if (!Array.isArray(actions)) return false;
21096
+ for (const action of actions) {
20878
21097
  if (!action || typeof action !== "object") continue;
20879
21098
  const a = action;
20880
21099
  if (a["Type"] !== "forward") continue;
@@ -20907,6 +21126,16 @@ function parsePort(raw) {
20907
21126
  function parseContainerPort(raw) {
20908
21127
  return parsePort(raw);
20909
21128
  }
21129
+ /**
21130
+ * Parse a ListenerRule `Priority` (ALB priorities are 1-50000, lower = higher
21131
+ * precedence). A missing / unparseable priority sorts last so an explicitly
21132
+ * prioritized rule always wins over it.
21133
+ */
21134
+ function parsePriority(raw) {
21135
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
21136
+ if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
21137
+ return Number.MAX_SAFE_INTEGER;
21138
+ }
20910
21139
 
20911
21140
  //#endregion
20912
21141
  //#region src/cli/commands/local-start-alb.ts
@@ -20988,31 +21217,52 @@ function albStrategy(options) {
20988
21217
  pickerNoun: "Application Load Balancers",
20989
21218
  onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-alb requires at least one <target>. Pass one or more ALB paths like 'Stack/MyAlb', or run it in a TTY to pick interactively.`),
20990
21219
  resolveBoots: (stacks, chosenTargets) => {
20991
- const byServiceTarget = /* @__PURE__ */ new Map();
20992
21220
  const warnings = [];
21221
+ const serviceTargets = /* @__PURE__ */ new Set();
21222
+ const listeners = [];
21223
+ const claimedHostPorts = /* @__PURE__ */ new Map();
20993
21224
  for (const albTarget of chosenTargets) {
20994
21225
  const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
20995
21226
  const resolution = resolveAlbFrontDoor(stack, albLogicalId);
20996
21227
  warnings.push(...resolution.warnings);
20997
- for (const svc of resolution.services) {
20998
- const target = `${stack.stackName}:${svc.serviceLogicalId}`;
20999
- const existing = byServiceTarget.get(target);
21000
- if (existing) existing.frontDoorTargets.push(...svc.targets);
21001
- else byServiceTarget.set(target, {
21002
- target,
21003
- frontDoorTargets: [...svc.targets]
21228
+ const qualify = (t) => {
21229
+ const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
21230
+ serviceTargets.add(serviceTarget);
21231
+ return {
21232
+ serviceTarget,
21233
+ targetContainerName: t.targetContainerName,
21234
+ targetContainerPort: t.targetContainerPort
21235
+ };
21236
+ };
21237
+ for (const listener of resolution.listeners) {
21238
+ const hostPort = lbPortOverrides[listener.listenerPort] ?? listener.listenerPort;
21239
+ const claimedBy = claimedHostPorts.get(hostPort);
21240
+ if (claimedBy !== void 0) {
21241
+ warnings.push(`Listener port ${listener.listenerPort} would bind host port ${hostPort}, already claimed by listener port ${claimedBy}; the local front-door fronts only the first. Use --lb-port to remap one of them.`);
21242
+ continue;
21243
+ }
21244
+ claimedHostPorts.set(hostPort, listener.listenerPort);
21245
+ listeners.push({
21246
+ listenerPort: listener.listenerPort,
21247
+ hostPort,
21248
+ ...listener.defaultTarget ? { defaultTarget: qualify(listener.defaultTarget) } : {},
21249
+ rules: listener.rules.map((r) => ({
21250
+ priority: r.priority,
21251
+ pathPatterns: r.pathPatterns,
21252
+ target: qualify(r.target)
21253
+ }))
21004
21254
  });
21005
21255
  }
21006
21256
  }
21007
- const boots = [...byServiceTarget.values()];
21008
- const resolvedPorts = /* @__PURE__ */ new Set();
21009
- for (const b of boots) for (const t of b.frontDoorTargets) resolvedPorts.add(t.listenerPort);
21257
+ const boots = [...serviceTargets].map((target) => ({ target }));
21258
+ const resolvedPorts = new Set(listeners.map((l) => l.listenerPort));
21010
21259
  for (const portStr of Object.keys(lbPortOverrides)) {
21011
21260
  const port = Number(portStr);
21012
21261
  if (!resolvedPorts.has(port)) warnings.push(`--lb-port override for listener port ${port} matched no ALB listener resolved for the named target(s); it was ignored.`);
21013
21262
  }
21014
21263
  return {
21015
21264
  boots,
21265
+ ...listeners.length > 0 ? { frontDoor: { listeners } } : {},
21016
21266
  warnings
21017
21267
  };
21018
21268
  },
@@ -21028,7 +21278,7 @@ function albStrategy(options) {
21028
21278
  */
21029
21279
  function createLocalStartAlbCommand(opts = {}) {
21030
21280
  setEmbedConfig(opts.embedConfig);
21031
- 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 forward listeners and stands up a local front-door on each listener port that round-robins across the running replicas — a single 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. v1 supports a single default-action forward to an HTTP listener; HTTPS listeners and Lambda target groups are skipped with a warning, and listener-rule routing is tracked separately. 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) => {
21281
+ 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 path-routes its path-pattern 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, single-target forward actions, and path-pattern rules; HTTPS listeners, Lambda target groups, weighted forwards, other rule conditions, and redirect/fixed-response 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) => {
21032
21282
  await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
21033
21283
  })));
21034
21284
  }
@@ -21105,4 +21355,4 @@ function createLocalListCommand(opts = {}) {
21105
21355
 
21106
21356
  //#endregion
21107
21357
  export { createJwksCache as $, invokeTokenAuthorizer as A, resolveCfnRegion as At, VtlEvaluationError as B, parseSelectionExpressionPath as Bt, resolveSelectionExpression as C, resolveEnvVars as Ct, computeRequestIdentityHash as D, isCfnFlagPresent as Dt, buildMethodArn as E, createLocalStateProvider as Et, buildRestV1Event as F, resolveWatchConfig as Ft, buildMgmtEndpointEnvUrl as G, AGENTCORE_RUNTIME_TYPE as Gt, probeHostGatewaySupport as H, pickRefLogicalId as Ht, evaluateResponseParameters as I, countTargets as It, buildConnectEvent as J, derivePseudoParametersFromRegion as Jt, handleConnectionsRequest as K, AgentCoreResolutionError as Kt, pickResponseTemplate as L, listTargets as Lt, translateLambdaResponse as M, CfnLocalStateProvider as Mt, applyAuthorizerOverlay as N, collectSsmParameterRefs as Nt, evaluateCachedLambdaPolicy as O, rejectExplicitCfnStackWithMultipleStacks as Ot, buildHttpApiV2Event as P, resolveSsmParameters as Pt, buildJwksUrlFromIssuer as Q, selectIntegrationResponse as R, discoverWebSocketApis as Rt, startApiServer as S, substituteEnvVarsFromStateAsync as St, defaultCredentialsLoader as T, LocalStateSourceError as Tt, bufferToBody as U, resolveLambdaArnIntrinsic as Ut, HOST_GATEWAY_MIN_VERSION as V, discoverRoutes as Vt, ConnectionRegistry as W, AGENTCORE_HTTP_PROTOCOL as Wt, buildMessageEvent as X, tryResolveImageFnJoin as Xt, buildDisconnectEvent as Y, substituteImagePlaceholders as Yt, buildCognitoJwksUrl as Z, LocalInvokeBuildError as Zt, availableApiIdentifiers as _, resolveRuntimeImage as _t, buildCloudMapIndex as a, buildCorsConfigByApiId as at, groupRoutesByServer as b, substituteAgainstStateAsync as bt, createLocalRunTaskCommand as c, matchPreflight as ct, createWatchPredicates as d, waitForAgentCorePing as dt, verifyCognitoJwt as et, resolveApiTargetSubset as f, createLocalInvokeCommand as ft, buildStageMap as g, resolveRuntimeFileExtension as gt, attachStageContext as h, resolveRuntimeCodeMountPath as ht, createLocalStartServiceCommand as i, applyCorsResponseHeaders as it, matchRoute as j, resolveCfnStackName as jt, invokeRequestAuthorizer as k, resolveCfnFallbackRegion as kt, createLocalInvokeAgentCoreCommand as l, AGENTCORE_SESSION_ID_HEADER as lt, createFileWatcher as m, buildContainerImage as mt, formatTargetListing as n, verifyJwtViaDiscovery as nt, CloudMapRegistry as o, buildCorsConfigFromCloudFrontChain as ot, createAuthorizerCache as p, architectureToPlatform as pt, parseConnectionsPath as q, resolveAgentCoreTarget as qt, createLocalStartAlbCommand as r, attachAuthorizers as rt, getContainerNetworkIp as s, isFunctionUrlOacFronted as st, createLocalListCommand as t, verifyJwtAuthorizer as tt, createLocalStartApiCommand as u, invokeAgentCore as ut, filterRoutesByApiIdentifier as v, EcsTaskResolutionError as vt, resolveServiceIntegrationParameters as w, materializeLayerFromArn as wt, readMtlsMaterialsFromDisk as x, substituteEnvVarsFromState as xt, filterRoutesByApiIdentifiers as y, substituteAgainstState as yt, tryParseStatus as z, discoverWebSocketApisOrThrow as zt };
21108
- //# sourceMappingURL=local-list-CVAyM_lE.js.map
21358
+ //# sourceMappingURL=local-list-DDsa8FJV.js.map