@go-to-k/cdkd 0.132.2 → 0.132.3

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) {
@@ -45522,11 +45574,26 @@ async function handleRequest(req, res, state, opts) {
45522
45574
  *
45523
45575
  * Cookies in the prelude are emitted as multiple `Set-Cookie` headers
45524
45576
  * (HTTP API v2 semantics — matching the buffered path's behavior).
45577
+ *
45578
+ * **Conflicting headers stripped**: `Content-Length` and
45579
+ * `Transfer-Encoding` (case-insensitive) are removed from the prelude
45580
+ * before `res.writeHead(...)` — some handlers set these defensively,
45581
+ * but doing so either crashes Node (Content-Length doesn't match the
45582
+ * actual bytes that arrive on the chunked body) or produces a
45583
+ * corrupted Content-Length-but-actually-chunked wire response. Node
45584
+ * automatically emits `Transfer-Encoding: chunked` when no
45585
+ * Content-Length is set, which is what streaming Function URLs
45586
+ * always want.
45525
45587
  */
45526
45588
  function writeStreamingResponse(res, result, releasePool) {
45527
45589
  const logger = getLogger().child("start-api");
45528
45590
  const { prelude, body } = result;
45529
- const headersOut = { ...prelude.headers };
45591
+ const headersOut = {};
45592
+ for (const [key, value] of Object.entries(prelude.headers)) {
45593
+ const lower = key.toLowerCase();
45594
+ if (lower === "content-length" || lower === "transfer-encoding") continue;
45595
+ headersOut[key] = value;
45596
+ }
45530
45597
  if (prelude.cookies && prelude.cookies.length > 0) headersOut["set-cookie"] = prelude.cookies;
45531
45598
  res.writeHead(prelude.statusCode, headersOut);
45532
45599
  let released = false;
@@ -51707,7 +51774,7 @@ function reorderArgs(argv) {
51707
51774
  */
51708
51775
  async function main() {
51709
51776
  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");
51777
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.132.3");
51711
51778
  program.addCommand(createBootstrapCommand());
51712
51779
  program.addCommand(createSynthCommand());
51713
51780
  program.addCommand(createListCommand());