@go-to-k/cdkd 0.132.2 → 0.132.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -39510,6 +39510,24 @@ const STREAM_PRELUDE_SEPARATOR = Buffer.from([
39510
39510
  */
39511
39511
  const STREAM_PRELUDE_MAX_BYTES = 1024 * 1024;
39512
39512
  /**
39513
+ * Maximum cumulative bytes of body the streaming Readable will push
39514
+ * before destroying itself with a clear error (defense-in-depth against
39515
+ * a buggy / malicious handler streaming gigabytes — Node's chunked-pipe
39516
+ * machinery handles per-chunk backpressure, but the running total can
39517
+ * still grow without bound on a slow consumer).
39518
+ *
39519
+ * 100 MiB is the default cap — generous enough that no realistic
39520
+ * dev-loop streaming response (token-by-token LLM output, large-file
39521
+ * download, video segment) hits it, low enough that a genuine runaway
39522
+ * surfaces locally before swap pressure kicks in. The HTTP server
39523
+ * converts the destroyed Readable to a truncated response (best-effort —
39524
+ * headers may already be on the wire).
39525
+ *
39526
+ * Consistent with {@link STREAM_PRELUDE_MAX_BYTES} (1 MiB cap on the
39527
+ * pre-body buffer); this is the post-body counterpart.
39528
+ */
39529
+ const STREAM_BODY_MAX_BYTES = 100 * 1024 * 1024;
39530
+ /**
39513
39531
  * POST the event payload to RIE with the `streaming` response-mode
39514
39532
  * header, parse the JSON prelude out of the response bytes, and return
39515
39533
  * a Readable carrying the post-separator body chunks.
@@ -39525,10 +39543,26 @@ const STREAM_PRELUDE_MAX_BYTES = 1024 * 1024;
39525
39543
  * the header, but setting it makes the contract explicit and survives
39526
39544
  * future RIE behavior changes.)
39527
39545
  *
39528
- * `timeoutMs` bounds the total wall time including the prelude wait —
39529
- * the handler can stream for the full Lambda timeout, so callers should
39530
- * pass a generous value (typically the function's configured Timeout * 2
39531
- * with a 30s floor, matching `invokeRie`'s convention).
39546
+ * **`timeoutMs` bounds the TOTAL wall time of the entire streaming
39547
+ * exchange**, NOT just the prelude wait the single armed `setTimeout`
39548
+ * covers both the prelude arrival AND the body drain. Once it fires,
39549
+ * `controller.abort()` destroys the underlying Readable, so a
39550
+ * legitimately long-lived streaming handler (e.g. a 15-minute AI / LLM
39551
+ * proxy) will have its connection torn down mid-stream even though
39552
+ * bytes are arriving correctly. Callers MUST size `timeoutMs` to cover
39553
+ * the longest expected handler stream, NOT just the time to first byte.
39554
+ *
39555
+ * Convention: pass `lambda.Timeout * 2` with a 30-second floor — same
39556
+ * order-of-magnitude formula as `invokeRie`, but the absolute value
39557
+ * differs because streaming handlers can intentionally run for the full
39558
+ * Lambda timeout (default 15 minutes for streaming-capable functions).
39559
+ * Splitting the bound into a strict prelude timer + a per-chunk idle
39560
+ * timer that resets on each chunk is deferred to a follow-up — see
39561
+ * issue #503 item 1 for the design discussion.
39562
+ *
39563
+ * The body Readable is additionally guarded by {@link STREAM_BODY_MAX_BYTES}
39564
+ * (100 MiB by default) so a runaway handler can't blow host memory; the
39565
+ * Readable is destroyed with a clear error when the cap trips.
39532
39566
  */
39533
39567
  async function invokeRieStreaming(host, port, event, timeoutMs) {
39534
39568
  const url = `http://${host}:${port}${INVOKE_PATH}`;
@@ -39562,7 +39596,7 @@ async function invokeRieStreaming(host, port, event, timeoutMs) {
39562
39596
  preludeBytes = preludeBytes.subarray(0, separatorIdx);
39563
39597
  break;
39564
39598
  }
39565
- if (preludeBytes.length > STREAM_PRELUDE_MAX_BYTES) {
39599
+ if (preludeBytes.length > 1048576) {
39566
39600
  clearTimeout(timer);
39567
39601
  reader.cancel().catch(() => void 0);
39568
39602
  throw new Error(`RIE streaming response did not emit the prelude/body separator within ${STREAM_PRELUDE_MAX_BYTES} bytes. The handler likely did not call awslambda.HttpResponseStream.from(stream, metadata).`);
@@ -39581,13 +39615,31 @@ async function invokeRieStreaming(host, port, event, timeoutMs) {
39581
39615
  throw new Error(`RIE streaming response prelude is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
39582
39616
  }
39583
39617
  const stream = new Readable({ read() {} });
39618
+ let bodyBytesPushed = 0;
39619
+ const exceedsCap = (added) => {
39620
+ bodyBytesPushed += added;
39621
+ return bodyBytesPushed > STREAM_BODY_MAX_BYTES;
39622
+ };
39584
39623
  (async () => {
39585
39624
  try {
39586
- if (bodyTail && bodyTail.length > 0) stream.push(bodyTail);
39625
+ if (bodyTail && bodyTail.length > 0) {
39626
+ if (exceedsCap(bodyTail.length)) {
39627
+ reader.cancel().catch(() => void 0);
39628
+ stream.destroy(/* @__PURE__ */ new Error(`RIE streaming body exceeded ${STREAM_BODY_MAX_BYTES} bytes — destroying stream.`));
39629
+ return;
39630
+ }
39631
+ stream.push(bodyTail);
39632
+ }
39587
39633
  while (true) {
39588
39634
  const { value, done } = await reader.read();
39589
39635
  if (done) break;
39590
- stream.push(Buffer.from(value));
39636
+ const chunk = Buffer.from(value);
39637
+ if (exceedsCap(chunk.length)) {
39638
+ reader.cancel().catch(() => void 0);
39639
+ stream.destroy(/* @__PURE__ */ new Error(`RIE streaming body exceeded ${STREAM_BODY_MAX_BYTES} bytes — destroying stream.`));
39640
+ return;
39641
+ }
39642
+ stream.push(chunk);
39591
39643
  }
39592
39644
  stream.push(null);
39593
39645
  } catch (err) {
@@ -41715,6 +41767,23 @@ function compareValues(lhs, rhs, op) {
41715
41767
  case ">=": return sa >= sb;
41716
41768
  }
41717
41769
  }
41770
+ /**
41771
+ * `==` / `!=` semantics for the VTL evaluator. Mirrors Apache Velocity's
41772
+ * lenient equality: same-type primitives compare via `===`; nested objects
41773
+ * / arrays compare structurally via canonical JSON; cross-type pairs (the
41774
+ * common case in AWS API Gateway templates, e.g. `#if($x == "true")` where
41775
+ * `$x` is a boolean from `$input.json('$.flag')`) fall back to comparing
41776
+ * each side coerced through {@link safeStringify}. The cross-type fall-back
41777
+ * is what Velocity itself does in this context, NOT JavaScript's
41778
+ * `==` operator (which has its own well-known foot-guns around `null` /
41779
+ * `undefined` / `NaN`). See the Velocity reference for the canonical
41780
+ * semantics: <https://velocity.apache.org/engine/2.0/vtl-reference.html#a3.4.4.RelationalOperators>
41781
+ * (look for the "Equivalent" / "Not Equivalent" rows).
41782
+ *
41783
+ * Both `null` and `undefined` collapse to a single sentinel — two
41784
+ * unset variables are considered equal. This matches Velocity's lenient
41785
+ * `null` handling (Issue (#507) item 8).
41786
+ */
41718
41787
  function looseEqual(a, b) {
41719
41788
  if (a === b) return true;
41720
41789
  if (a == null || b == null) return a == null && b == null;
@@ -41755,6 +41824,19 @@ function isTruthy(v) {
41755
41824
  * `json(jsonPath)` returns a JSON-stringified slice of the parsed body;
41756
41825
  * `path(jsonPath)` returns the raw native value (primitives unquoted).
41757
41826
  *
41827
+ * Case-sensitivity contract (Issue (#507) item 5; mirrored on the write
41828
+ * side by `rest-v1-integrations.ts` `resolveRequestParameterValue` /
41829
+ * `applyRequestParameters`):
41830
+ *
41831
+ * - `$input.params('<name>')` resolves PATH (case-sensitive) →
41832
+ * QUERYSTRING (case-sensitive, last-wins on duplicates) → HEADER
41833
+ * (case-insensitive — first the input's pre-lowercased map, then a
41834
+ * last-resort case-insensitive scan; multi-value duplicates are
41835
+ * comma-joined by the http-server before reaching VTL).
41836
+ * - `$input.params('header')` / `$input.params('querystring')` /
41837
+ * `$input.params('path')` return the raw category maps without
41838
+ * case-folding the keys further.
41839
+ *
41758
41840
  * JSONPath support is minimal: supports `$` (root), `$.field`,
41759
41841
  * `$.field.subField`, `$.array[0]`. AWS supports more (filter
41760
41842
  * expressions, recursive descent); cdkd surfaces a clear error on
@@ -41763,20 +41845,24 @@ function isTruthy(v) {
41763
41845
  function buildVtlInput(body, headers, querystring, pathParams) {
41764
41846
  let jsonBodyCache;
41765
41847
  let jsonBodyParsed = false;
41766
- function lazyJson() {
41848
+ let jsonBodyParseError = false;
41849
+ function lazyJson(opts) {
41767
41850
  if (!jsonBodyParsed) {
41768
41851
  jsonBodyParsed = true;
41769
- try {
41770
- jsonBodyCache = body.length === 0 ? null : JSON.parse(body);
41852
+ if (body.length === 0) jsonBodyCache = null;
41853
+ else try {
41854
+ jsonBodyCache = JSON.parse(body);
41771
41855
  } catch {
41772
41856
  jsonBodyCache = null;
41857
+ jsonBodyParseError = true;
41773
41858
  }
41774
41859
  }
41860
+ if (jsonBodyParseError && opts?.throwOnParseError) throw new VtlEvaluationError("$input.json(...) cannot be evaluated: request body is not valid JSON. On AWS API Gateway this surface returns HTTP 400 with {\"message\":\"Invalid JSON request body\"}; use $input.body or $input.path(...) when the upstream may emit non-JSON content.");
41775
41861
  return jsonBodyCache;
41776
41862
  }
41777
41863
  function jsonFn(...args) {
41778
41864
  const expr = args.length > 0 ? String(args[0]) : "$";
41779
- const val = applyJsonPath(lazyJson(), expr);
41865
+ const val = applyJsonPath(lazyJson({ throwOnParseError: true }), expr);
41780
41866
  return JSON.stringify(val ?? null);
41781
41867
  }
41782
41868
  function pathFn(...args) {
@@ -41916,12 +42002,17 @@ function selectIntegrationResponse(entries, matchTarget, fallbackStatusCode = 20
41916
42002
  };
41917
42003
  }
41918
42004
  function parseStatus$1(raw, fallback) {
41919
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
41920
- if (typeof raw === "string") {
41921
- const parsed = Number.parseInt(raw, 10);
41922
- if (Number.isFinite(parsed)) return parsed;
41923
- }
41924
- return fallback;
42005
+ if (typeof raw === "number") {
42006
+ if (Number.isInteger(raw) && raw >= 100 && raw < 600) return raw;
42007
+ return fallback;
42008
+ }
42009
+ if (typeof raw !== "string") return fallback;
42010
+ const trimmed = raw.trim();
42011
+ if (trimmed === "") return fallback;
42012
+ const parsed = Number(trimmed);
42013
+ if (!Number.isInteger(parsed)) return fallback;
42014
+ if (parsed < 100 || parsed >= 600) return fallback;
42015
+ return parsed;
41925
42016
  }
41926
42017
  /**
41927
42018
  * Evaluate `IntegrationResponse.ResponseParameters` — header literals
@@ -41933,6 +42024,17 @@ function parseStatus$1(raw, fallback) {
41933
42024
  * are `'literal'` (with single quotes) or mapping expressions
41934
42025
  * (`integration.response.body.X` / `integration.response.header.X` /
41935
42026
  * `context.X`). cdkd v1 supports the literal form only.
42027
+ *
42028
+ * PR #511 review fix-back: header names are lowercased here so the
42029
+ * returned map shares the same key namespace as the dispatcher's
42030
+ * default-initialized headers (`{'content-type': '...'}`). Without
42031
+ * normalization a template that sets `Content-Type` PascalCase via
42032
+ * ResponseParameters produced a headers object carrying BOTH
42033
+ * `'content-type': 'application/json'` (default) AND
42034
+ * `'Content-Type': 'text/xml'` (overlay), which downstream HTTP
42035
+ * serialization rendered as two conflicting headers — AWS-deployed
42036
+ * only ever returns one. By lowercasing every key, overlays simply
42037
+ * overwrite the default-initializer entry like AWS does.
41936
42038
  */
41937
42039
  function evaluateResponseParameters(responseParameters, opts = {}) {
41938
42040
  if (!responseParameters) return {};
@@ -41943,7 +42045,7 @@ function evaluateResponseParameters(responseParameters, opts = {}) {
41943
42045
  opts.onUnsupported?.(key, value, `Only method.response.header.<name> keys are supported on REST v1 ResponseParameters; cdkd cannot map ${key}.`);
41944
42046
  continue;
41945
42047
  }
41946
- const headerName = headerMatch[1];
42048
+ const headerName = headerMatch[1].toLowerCase();
41947
42049
  if (typeof value !== "string") {
41948
42050
  opts.onUnsupported?.(key, String(value), `non-string ResponseParameter value`);
41949
42051
  continue;
@@ -42039,6 +42141,7 @@ function dispatchMockIntegration(config, req) {
42039
42141
  }
42040
42142
  const headers = { "content-type": contentType };
42041
42143
  Object.assign(headers, evaluateResponseParameters(entry.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`MOCK response: ${reason}`) }));
42144
+ if (body === "" && headers["content-type"] === contentType) delete headers["content-type"];
42042
42145
  return {
42043
42146
  statusCode: parseStatus(entry.StatusCode) ?? 200,
42044
42147
  headers,
@@ -42061,10 +42164,7 @@ async function dispatchHttpProxyIntegration(config, req, deps) {
42061
42164
  "content-length",
42062
42165
  "transfer-encoding"
42063
42166
  ]) delete outHeaders[drop];
42064
- applyRequestParameters(config.requestParameters, req, {
42065
- headers: outHeaders,
42066
- urlObj: void 0
42067
- });
42167
+ applyRequestParameters(config.requestParameters, req, { headers: outHeaders });
42068
42168
  const fetchImpl = deps.fetch ?? globalThis.fetch;
42069
42169
  const fetchInit = {
42070
42170
  method,
@@ -42132,10 +42232,7 @@ async function dispatchHttpIntegration(config, req, deps) {
42132
42232
  "content-length",
42133
42233
  "transfer-encoding"
42134
42234
  ]) delete outHeaders[drop];
42135
- applyRequestParameters(config.requestParameters, req, {
42136
- headers: outHeaders,
42137
- urlObj: void 0
42138
- });
42235
+ applyRequestParameters(config.requestParameters, req, { headers: outHeaders });
42139
42236
  const fetchImpl = deps.fetch ?? globalThis.fetch;
42140
42237
  const fetchInit = {
42141
42238
  method,
@@ -42230,46 +42327,48 @@ async function dispatchAwsLambdaIntegration(config, req, deps) {
42230
42327
  })
42231
42328
  };
42232
42329
  }
42233
- let invokeOutcome;
42234
42330
  try {
42235
- invokeOutcome = await invokeRie(handle.containerHost, handle.hostPort, eventPayload, deps.rieTimeoutMs);
42236
- } catch (err) {
42237
- deps.pool.release(handle);
42331
+ let invokeOutcome;
42332
+ try {
42333
+ invokeOutcome = await invokeRie(handle.containerHost, handle.hostPort, eventPayload, deps.rieTimeoutMs);
42334
+ } catch (err) {
42335
+ return {
42336
+ statusCode: 502,
42337
+ headers: { "content-type": "application/json" },
42338
+ body: JSON.stringify({
42339
+ message: "AWS Lambda non-proxy invocation failed",
42340
+ reason: err instanceof Error ? err.message : String(err)
42341
+ })
42342
+ };
42343
+ }
42344
+ const payload = invokeOutcome.payload;
42345
+ const isError = payload !== null && typeof payload === "object" && "errorMessage" in payload;
42346
+ const matchTarget = isError ? String(payload["errorMessage"]) : "success";
42347
+ const selected = selectIntegrationResponse(config.responses, matchTarget, isError ? 500 : 200);
42348
+ const respCtx = buildVtlContextFromRequest(req, JSON.stringify(payload ?? null), payload);
42349
+ let body = "";
42350
+ let contentType = "application/json";
42351
+ if (selected.entry) {
42352
+ const picked = pickResponseTemplate(selected.entry.ResponseTemplates, req.headers["accept"]);
42353
+ if (picked) {
42354
+ try {
42355
+ body = evaluateVtl(picked.template, respCtx);
42356
+ } catch (err) {
42357
+ return vtlFailure("response", err, picked.template);
42358
+ }
42359
+ contentType = picked.contentType;
42360
+ } else body = JSON.stringify(payload ?? null);
42361
+ } else body = JSON.stringify(payload ?? null);
42362
+ const headers = { "content-type": contentType };
42363
+ Object.assign(headers, evaluateResponseParameters(selected.entry?.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`AWS Lambda non-proxy response: ${reason}`) }));
42238
42364
  return {
42239
- statusCode: 502,
42240
- headers: { "content-type": "application/json" },
42241
- body: JSON.stringify({
42242
- message: "AWS Lambda non-proxy invocation failed",
42243
- reason: err instanceof Error ? err.message : String(err)
42244
- })
42365
+ statusCode: selected.statusCode,
42366
+ headers,
42367
+ body
42245
42368
  };
42369
+ } finally {
42370
+ deps.pool.release(handle);
42246
42371
  }
42247
- deps.pool.release(handle);
42248
- const payload = invokeOutcome.payload;
42249
- const isError = payload !== null && typeof payload === "object" && "errorMessage" in payload;
42250
- const matchTarget = isError ? String(payload["errorMessage"]) : "success";
42251
- const selected = selectIntegrationResponse(config.responses, matchTarget, isError ? 500 : 200);
42252
- const respCtx = buildVtlContextFromRequest(req, JSON.stringify(payload ?? null), payload);
42253
- let body = "";
42254
- let contentType = "application/json";
42255
- if (selected.entry) {
42256
- const picked = pickResponseTemplate(selected.entry.ResponseTemplates, req.headers["accept"]);
42257
- if (picked) {
42258
- try {
42259
- body = evaluateVtl(picked.template, respCtx);
42260
- } catch (err) {
42261
- return vtlFailure("response", err, picked.template);
42262
- }
42263
- contentType = picked.contentType;
42264
- } else body = JSON.stringify(payload ?? null);
42265
- } else body = JSON.stringify(payload ?? null);
42266
- const headers = { "content-type": contentType };
42267
- Object.assign(headers, evaluateResponseParameters(selected.entry?.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`AWS Lambda non-proxy response: ${reason}`) }));
42268
- return {
42269
- statusCode: selected.statusCode,
42270
- headers,
42271
- body
42272
- };
42273
42372
  }
42274
42373
  function buildVtlContextFromRequest(req, body, inputRoot) {
42275
42374
  return {
@@ -42310,29 +42409,57 @@ function pickRequestTemplate(requestTemplates, contentType) {
42310
42409
  /**
42311
42410
  * Extract `{"statusCode": <N>}` from a rendered MOCK request template.
42312
42411
  * AWS uses this single key to drive `IntegrationResponses[]` selection.
42412
+ *
42413
+ * Returns `undefined` when the rendered template is not JSON OR does not
42414
+ * carry a `{statusCode: N}` object OR the value is not a positive integer
42415
+ * (Issue (#507) item 6 — `Number.isInteger` rejects `"200abc"` /
42416
+ * fractional values that `Number.parseInt` would silently accept). On any
42417
+ * fallback case the caller falls back to the default `IntegrationResponses[]`
42418
+ * entry. The fallback path is logged at debug (Issue (#507) item 3) so
42419
+ * users diagnosing a MOCK dispatch see what AWS would have seen.
42313
42420
  */
42314
42421
  function extractStatusCodeFromRendered(rendered) {
42422
+ const logFallback = (reason) => {
42423
+ const truncated = rendered.length > 200 ? rendered.slice(0, 200) + "..." : rendered;
42424
+ getLogger().child("start-api").debug(`MOCK request template did not yield a statusCode selection driver (${reason}); falling back to the default IntegrationResponses[] entry. Rendered output: ${truncated}`);
42425
+ };
42426
+ let parsed;
42315
42427
  try {
42316
- const parsed = JSON.parse(rendered);
42317
- if (parsed && typeof parsed === "object" && "statusCode" in parsed) {
42318
- const val = parsed["statusCode"];
42319
- if (typeof val === "number") return val;
42320
- if (typeof val === "string") {
42321
- const n = Number.parseInt(val, 10);
42322
- if (Number.isFinite(n)) return n;
42323
- }
42324
- }
42325
- } catch {}
42428
+ parsed = JSON.parse(rendered);
42429
+ } catch {
42430
+ return logFallback("rendered output is not valid JSON");
42431
+ }
42432
+ if (!parsed || typeof parsed !== "object" || !("statusCode" in parsed)) return logFallback("rendered output has no statusCode field");
42433
+ const val = parsed["statusCode"];
42434
+ if (typeof val === "number") {
42435
+ if (Number.isInteger(val) && val >= 100 && val < 600) return val;
42436
+ return logFallback(`statusCode ${val} is out of HTTP range [100, 600)`);
42437
+ }
42438
+ if (typeof val === "string") {
42439
+ const trimmed = val.trim();
42440
+ if (trimmed === "") return logFallback("statusCode is empty / whitespace");
42441
+ const n = Number(trimmed);
42442
+ if (!Number.isInteger(n)) return logFallback(`statusCode '${val}' is not a valid integer`);
42443
+ if (n < 100 || n >= 600) return logFallback(`statusCode ${n} is out of HTTP range [100, 600)`);
42444
+ return n;
42445
+ }
42446
+ return logFallback(`statusCode has unexpected type '${typeof val}'`);
42326
42447
  }
42327
42448
  function defaultResponseEntry(entries) {
42328
42449
  return entries.find((e) => e.SelectionPattern === void 0 || e.SelectionPattern === "") ?? null;
42329
42450
  }
42330
42451
  function parseStatus(raw) {
42331
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
42332
- if (typeof raw === "string") {
42333
- const n = Number.parseInt(raw, 10);
42334
- if (Number.isFinite(n)) return n;
42452
+ if (typeof raw === "number") {
42453
+ if (Number.isInteger(raw) && raw >= 100 && raw < 600) return raw;
42454
+ return;
42335
42455
  }
42456
+ if (typeof raw !== "string") return void 0;
42457
+ const trimmed = raw.trim();
42458
+ if (trimmed === "") return void 0;
42459
+ const n = Number(trimmed);
42460
+ if (!Number.isInteger(n)) return void 0;
42461
+ if (n < 100 || n >= 600) return void 0;
42462
+ return n;
42336
42463
  }
42337
42464
  /**
42338
42465
  * Heuristic: is the given HTTP `Content-Type` header value likely to
@@ -42411,10 +42538,12 @@ function safeJsonParse(s) {
42411
42538
  *
42412
42539
  * Supported key shapes:
42413
42540
  * - `integration.request.header.<name>` → outgoing header
42414
- * - `integration.request.querystring.<name>` → query string param
42415
- * - `integration.request.path.<name>` → path placeholder substitution
42541
+ * - `integration.request.querystring.<name>` → query string param (warn-and-skip)
42542
+ * - `integration.request.path.<name>` → path placeholder substitution (warn-and-skip)
42416
42543
  *
42417
- * Supported value shapes:
42544
+ * Supported value shapes (header case-insensitive + multi-value comma-joined,
42545
+ * querystring case-sensitive + last-wins; see {@link resolveRequestParameterValue}
42546
+ * and `vtl-engine.ts` `$input.params` for the matching read-side semantics):
42418
42547
  * - `method.request.header.<name>` → read incoming header
42419
42548
  * - `method.request.querystring.<name>` → read incoming query param
42420
42549
  * - `method.request.path.<name>` → read path parameter
@@ -42422,6 +42551,12 @@ function safeJsonParse(s) {
42422
42551
  *
42423
42552
  * Unsupported mapping expressions are logged at warn and skipped (matches
42424
42553
  * the ResponseParameters handling in `integration-response-selector.ts`).
42554
+ *
42555
+ * Note: querystring / path-rewrite branches currently warn-and-skip; cdkd
42556
+ * relies on `{paramName}` URI substitution for the canonical case (see
42557
+ * {@link substituteUriPlaceholders}). The previous `urlObj` parameter was
42558
+ * never used by the unimplemented querystring rewrite branch and has been
42559
+ * dropped (Issue (#507) item 2).
42425
42560
  */
42426
42561
  function applyRequestParameters(requestParameters, req, out) {
42427
42562
  if (!requestParameters) return;
@@ -42441,6 +42576,22 @@ function applyRequestParameters(requestParameters, req, out) {
42441
42576
  else logger.warn(`Unsupported RequestParameter key '${key}'; skipping.`);
42442
42577
  }
42443
42578
  }
42579
+ /**
42580
+ * Resolve a single `RequestParameters` value to a string.
42581
+ *
42582
+ * Case-sensitivity contract (Issue (#507) item 5; mirrored on the VTL
42583
+ * read side by `vtl-engine.ts` `$input.params`):
42584
+ *
42585
+ * - Header lookups are **case-insensitive** (the incoming-header map is
42586
+ * pre-lowercased by the http-server) and multi-value duplicates are
42587
+ * comma-joined.
42588
+ * - Querystring lookups are **case-sensitive** (matches AWS API Gateway's
42589
+ * deployed behavior) and multi-value duplicates surface only the
42590
+ * last-wins string (the http-server's request snapshot collapses
42591
+ * duplicates at parse time).
42592
+ * - Path parameters are case-sensitive (CFn template `{paramName}`
42593
+ * placeholders are case-sensitive by construction).
42594
+ */
42444
42595
  function resolveRequestParameterValue(raw, req) {
42445
42596
  if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
42446
42597
  const headerMatch = /^method\.request\.header\.(.+)$/.exec(raw);
@@ -45522,11 +45673,26 @@ async function handleRequest(req, res, state, opts) {
45522
45673
  *
45523
45674
  * Cookies in the prelude are emitted as multiple `Set-Cookie` headers
45524
45675
  * (HTTP API v2 semantics — matching the buffered path's behavior).
45676
+ *
45677
+ * **Conflicting headers stripped**: `Content-Length` and
45678
+ * `Transfer-Encoding` (case-insensitive) are removed from the prelude
45679
+ * before `res.writeHead(...)` — some handlers set these defensively,
45680
+ * but doing so either crashes Node (Content-Length doesn't match the
45681
+ * actual bytes that arrive on the chunked body) or produces a
45682
+ * corrupted Content-Length-but-actually-chunked wire response. Node
45683
+ * automatically emits `Transfer-Encoding: chunked` when no
45684
+ * Content-Length is set, which is what streaming Function URLs
45685
+ * always want.
45525
45686
  */
45526
45687
  function writeStreamingResponse(res, result, releasePool) {
45527
45688
  const logger = getLogger().child("start-api");
45528
45689
  const { prelude, body } = result;
45529
- const headersOut = { ...prelude.headers };
45690
+ const headersOut = {};
45691
+ for (const [key, value] of Object.entries(prelude.headers)) {
45692
+ const lower = key.toLowerCase();
45693
+ if (lower === "content-length" || lower === "transfer-encoding") continue;
45694
+ headersOut[key] = value;
45695
+ }
45530
45696
  if (prelude.cookies && prelude.cookies.length > 0) headersOut["set-cookie"] = prelude.cookies;
45531
45697
  res.writeHead(prelude.statusCode, headersOut);
45532
45698
  let released = false;
@@ -51707,7 +51873,7 @@ function reorderArgs(argv) {
51707
51873
  */
51708
51874
  async function main() {
51709
51875
  const program = new Command();
51710
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.132.2");
51876
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.132.4");
51711
51877
  program.addCommand(createBootstrapCommand());
51712
51878
  program.addCommand(createSynthCommand());
51713
51879
  program.addCommand(createListCommand());