@go-to-k/cdkd 0.129.0 → 0.130.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -468,14 +468,21 @@ preflight (HTTP API v2 `CorsConfiguration` + REST v1 OPTIONS MOCK
468
468
  preflight from `defaultCorsPreflightOptions`); hot reload via `--watch`;
469
469
  deploy-state-backed env var substitution via `--from-state`.
470
470
 
471
+ Function URL `InvokeMode: RESPONSE_STREAM` is supported (issue #467):
472
+ streaming Lambdas are invoked via the RIE streaming protocol and the
473
+ response is piped to the HTTP client with `Transfer-Encoding: chunked`.
474
+ Note that AWS's local RIE buffers the response — incremental chunk
475
+ delivery only manifests against the deployed Lambda runtime; locally
476
+ the response shape is correct but arrives in one block.
477
+
471
478
  Routes whose integration cdkd cannot emulate (non-AWS_PROXY REST v1
472
479
  types other than the MOCK CORS preflight subset, HTTP API v2 service
473
- integrations, WebSocket APIs, Function URLs with IAM auth or
474
- RESPONSE_STREAM, cross-stack Lambda Arn references) **do not block
475
- boot** — the server starts with a per-route `[warn]` summary and
476
- returns HTTP 501 + the reason in the JSON body if and when the route is
477
- hit. This lets you run the rest of your API surface locally while the
478
- unsupported routes stay on the deployed API.
480
+ integrations, WebSocket APIs, Function URLs with IAM auth, cross-stack
481
+ Lambda Arn references) **do not block boot** — the server starts with
482
+ a per-route `[warn]` summary and returns HTTP 501 + the reason in the
483
+ JSON body if and when the route is hit. This lets you run the rest of
484
+ your API surface locally while the unsupported routes stay on the
485
+ deployed API.
479
486
 
480
487
  ### `local run-task`
481
488
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-CuHRHcyW.js";
3
- import { A as AssetPublisher, B as getLegacyStateBucketName, C as applyRoleArnIfSet, Ct as generateResourceNameWithFallback, D as LockManager, E as TemplateParser, F as getDockerCmd, G as resolveStateBucketWithDefaultAndSource, H as resolveCaptureObservedState, I as runDockerForeground, K as warnDeprecatedNoPrefixCliFlag, L as runDockerStreaming, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as formatDockerLoginError, R as Synthesizer, S as IntrinsicFunctionResolver, St as generateResourceName, T as DagBuilder, Tt as withStackName, U as resolveSkipPrefix, V as resolveApp, W as resolveStateBucketWithDefault, Y as resolveBucketRegion, Z as CdkdError, _ as normalizeAwsTagsToCfn, a as withRetry, at as ResourceUpdateNotSupportedError, b as CloudControlProvider, bt as PATTERN_B_NAME_PROPERTIES, c as cyan, ct as StackTerminationProtectionError, d as red, et as LocalInvokeBuildError, f as yellow, g as matchesCdkPath, gt as getLogger, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceTimeoutError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, mt as withErrorHandling, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as PartialFailureError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as RouteDiscoveryError, p as IAMRoleProvider, pt as normalizeAwsError, q as AssemblyReader, r as DeployEngine, rt as ProvisioningError, s as bold, st as StackHasActiveImportsError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as green, v as resolveExplicitPhysicalId, vt as runStackBuffered, w as DiffCalculator, wt as withSkipPrefix, x as assertRegionMatch, xt as PATTERN_B_RESOURCE_TYPES, y as ProviderRegistry, yt as getLiveRenderer, z as getDefaultStateBucketName } from "./deploy-engine-DWpeb9wT.js";
3
+ import { A as AssetPublisher, B as getLegacyStateBucketName, C as applyRoleArnIfSet, Ct as generateResourceNameWithFallback, D as LockManager, E as TemplateParser, F as getDockerCmd, G as resolveStateBucketWithDefaultAndSource, H as resolveCaptureObservedState, I as runDockerForeground, K as warnDeprecatedNoPrefixCliFlag, L as runDockerStreaming, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as formatDockerLoginError, R as Synthesizer, S as IntrinsicFunctionResolver, St as generateResourceName, T as DagBuilder, Tt as withStackName, U as resolveSkipPrefix, V as resolveApp, W as resolveStateBucketWithDefault, Y as resolveBucketRegion, Z as CdkdError, _ as normalizeAwsTagsToCfn, a as withRetry, at as ResourceUpdateNotSupportedError, b as CloudControlProvider, bt as PATTERN_B_NAME_PROPERTIES, c as cyan, ct as StackTerminationProtectionError, d as red, et as LocalInvokeBuildError, f as yellow, g as matchesCdkPath, gt as getLogger, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceTimeoutError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, mt as withErrorHandling, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as PartialFailureError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as RouteDiscoveryError, p as IAMRoleProvider, pt as normalizeAwsError, q as AssemblyReader, r as DeployEngine, rt as ProvisioningError, s as bold, st as StackHasActiveImportsError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as green, v as resolveExplicitPhysicalId, vt as runStackBuffered, w as DiffCalculator, wt as withSkipPrefix, x as assertRegionMatch, xt as PATTERN_B_RESOURCE_TYPES, y as ProviderRegistry, yt as getLiveRenderer, z as getDefaultStateBucketName } from "./deploy-engine-B-w4C_7O.js";
4
4
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
6
6
  import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
@@ -39464,13 +39464,16 @@ async function invokeRie(host, port, event, timeoutMs) {
39464
39464
  * something we should retry. Abort errors propagate immediately so the
39465
39465
  * outer timeout still wins.
39466
39466
  */
39467
- async function fetchWithStartupRetry(url, body, signal) {
39467
+ async function fetchWithStartupRetry(url, body, signal, extraHeaders) {
39468
39468
  const maxAttempts = 3;
39469
39469
  let lastError;
39470
39470
  for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
39471
39471
  return await fetch(url, {
39472
39472
  method: "POST",
39473
- headers: { "Content-Type": "application/json" },
39473
+ headers: {
39474
+ "Content-Type": "application/json",
39475
+ ...extraHeaders
39476
+ },
39474
39477
  body,
39475
39478
  signal
39476
39479
  });
@@ -39482,6 +39485,157 @@ async function fetchWithStartupRetry(url, body, signal) {
39482
39485
  }
39483
39486
  throw lastError;
39484
39487
  }
39488
+ /**
39489
+ * The 8-NULL-byte separator AWS Lambda RIE writes between the JSON
39490
+ * metadata prelude and the streaming body chunks. Empirically verified
39491
+ * — see `StreamingPrelude` docstring above.
39492
+ */
39493
+ const STREAM_PRELUDE_SEPARATOR = Buffer.from([
39494
+ 0,
39495
+ 0,
39496
+ 0,
39497
+ 0,
39498
+ 0,
39499
+ 0,
39500
+ 0,
39501
+ 0
39502
+ ]);
39503
+ /**
39504
+ * Maximum bytes we'll buffer searching for the prelude separator before
39505
+ * giving up. 1 MiB is far past anything Lambda's streamifyResponse would
39506
+ * emit as metadata (typical preludes are <500 bytes) — a runaway here
39507
+ * indicates the handler didn't call `HttpResponseStream.from` at all, in
39508
+ * which case we want to fail fast rather than buffer the whole body.
39509
+ */
39510
+ const STREAM_PRELUDE_MAX_BYTES = 1024 * 1024;
39511
+ /**
39512
+ * POST the event payload to RIE with the `streaming` response-mode
39513
+ * header, parse the JSON prelude out of the response bytes, and return
39514
+ * a Readable carrying the post-separator body chunks.
39515
+ *
39516
+ * Why a separate function from `invokeRie`: the prelude/separator/body
39517
+ * framing is incompatible with the buffered-response `text()` consumer.
39518
+ * Buffered routes still use `invokeRie`; only Function URLs with
39519
+ * `InvokeMode: RESPONSE_STREAM` use this path.
39520
+ *
39521
+ * The `Lambda-Runtime-Function-Response-Mode: streaming` request header
39522
+ * tells RIE we want the streaming protocol. (RIE happens to emit the
39523
+ * same protocol for `streamifyResponse`-wrapped handlers regardless of
39524
+ * the header, but setting it makes the contract explicit and survives
39525
+ * future RIE behavior changes.)
39526
+ *
39527
+ * `timeoutMs` bounds the total wall time including the prelude wait —
39528
+ * the handler can stream for the full Lambda timeout, so callers should
39529
+ * pass a generous value (typically the function's configured Timeout * 2
39530
+ * with a 30s floor, matching `invokeRie`'s convention).
39531
+ */
39532
+ async function invokeRieStreaming(host, port, event, timeoutMs) {
39533
+ const url = `http://${host}:${port}${INVOKE_PATH}`;
39534
+ const body = JSON.stringify(event ?? {});
39535
+ const controller = new AbortController();
39536
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
39537
+ let response;
39538
+ try {
39539
+ response = await fetchWithStartupRetry(url, body, controller.signal, { "Lambda-Runtime-Function-Response-Mode": "streaming" });
39540
+ } catch (err) {
39541
+ clearTimeout(timer);
39542
+ if (err.name === "AbortError") throw new Error(`RIE streaming invoke at ${url} timed out after ${timeoutMs}ms. The handler may be hung; check container logs.`);
39543
+ throw err;
39544
+ }
39545
+ if (!response.body) {
39546
+ clearTimeout(timer);
39547
+ throw new Error(`RIE streaming invoke at ${url} returned no response body.`);
39548
+ }
39549
+ const reader = response.body.getReader();
39550
+ let preludeBytes = Buffer.alloc(0);
39551
+ let bodyTail;
39552
+ let separatorIdx = -1;
39553
+ while (separatorIdx < 0) {
39554
+ const { value, done } = await reader.read();
39555
+ if (done) break;
39556
+ const chunk = Buffer.from(value);
39557
+ preludeBytes = Buffer.concat([preludeBytes, chunk]);
39558
+ separatorIdx = preludeBytes.indexOf(STREAM_PRELUDE_SEPARATOR);
39559
+ if (separatorIdx >= 0) {
39560
+ bodyTail = preludeBytes.subarray(separatorIdx + STREAM_PRELUDE_SEPARATOR.length);
39561
+ preludeBytes = preludeBytes.subarray(0, separatorIdx);
39562
+ break;
39563
+ }
39564
+ if (preludeBytes.length > STREAM_PRELUDE_MAX_BYTES) {
39565
+ clearTimeout(timer);
39566
+ reader.cancel().catch(() => void 0);
39567
+ throw new Error(`RIE streaming response did not emit the prelude/body separator within ${STREAM_PRELUDE_MAX_BYTES} bytes. The handler likely did not call awslambda.HttpResponseStream.from(stream, metadata).`);
39568
+ }
39569
+ }
39570
+ if (separatorIdx < 0) {
39571
+ clearTimeout(timer);
39572
+ throw new Error(`RIE streaming response ended before the prelude/body separator (got ${preludeBytes.length} bytes). The handler likely threw before streaming the prelude — check container logs.`);
39573
+ }
39574
+ let prelude;
39575
+ try {
39576
+ prelude = parseStreamingPrelude(preludeBytes.toString("utf8"));
39577
+ } catch (err) {
39578
+ clearTimeout(timer);
39579
+ reader.cancel().catch(() => void 0);
39580
+ throw new Error(`RIE streaming response prelude is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
39581
+ }
39582
+ const stream = new Readable({ read() {} });
39583
+ (async () => {
39584
+ try {
39585
+ if (bodyTail && bodyTail.length > 0) stream.push(bodyTail);
39586
+ while (true) {
39587
+ const { value, done } = await reader.read();
39588
+ if (done) break;
39589
+ stream.push(Buffer.from(value));
39590
+ }
39591
+ stream.push(null);
39592
+ } catch (err) {
39593
+ stream.destroy(err instanceof Error ? err : new Error(String(err)));
39594
+ } finally {
39595
+ clearTimeout(timer);
39596
+ }
39597
+ })();
39598
+ return {
39599
+ prelude,
39600
+ body: stream
39601
+ };
39602
+ }
39603
+ /**
39604
+ * Parse a streaming prelude payload (JSON text). Normalizes the shape
39605
+ * the http-server consumes: `statusCode` is coerced to a number (RIE
39606
+ * sometimes emits it as a string), `headers` is always an object (the
39607
+ * handler may omit it), `cookies` is preserved only when an array.
39608
+ *
39609
+ * Exported for unit tests. Throws on invalid JSON or a non-numeric
39610
+ * statusCode (cdkd cannot map that to HTTP).
39611
+ */
39612
+ function parseStreamingPrelude(text) {
39613
+ const trimmed = text.trim();
39614
+ if (trimmed.length === 0) throw new Error("empty prelude");
39615
+ const raw = JSON.parse(trimmed);
39616
+ if (!raw || typeof raw !== "object") throw new Error("prelude is not a JSON object");
39617
+ const obj = raw;
39618
+ const statusRaw = obj["statusCode"];
39619
+ let statusCode;
39620
+ if (typeof statusRaw === "number" && Number.isFinite(statusRaw)) statusCode = Math.trunc(statusRaw);
39621
+ else if (typeof statusRaw === "string" && /^[0-9]+$/.test(statusRaw)) statusCode = Number.parseInt(statusRaw, 10);
39622
+ else throw new Error(`statusCode must be a number (got ${typeof statusRaw})`);
39623
+ const headers = {};
39624
+ const headersRaw = obj["headers"];
39625
+ if (headersRaw && typeof headersRaw === "object") for (const [k, v] of Object.entries(headersRaw)) {
39626
+ if (v === null || v === void 0) continue;
39627
+ if (typeof v === "string") headers[k] = v;
39628
+ else if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") headers[k] = String(v);
39629
+ else headers[k] = JSON.stringify(v) ?? "";
39630
+ }
39631
+ const result = {
39632
+ statusCode,
39633
+ headers
39634
+ };
39635
+ const cookiesRaw = obj["cookies"];
39636
+ if (Array.isArray(cookiesRaw)) result.cookies = cookiesRaw.map((c) => String(c));
39637
+ return result;
39638
+ }
39485
39639
 
39486
39640
  //#endregion
39487
39641
  //#region src/assets/asset-manifest-loader.ts
@@ -40538,12 +40692,15 @@ function classifyServiceIntegrationRoute(baseRoute, integrationProps, stackName,
40538
40692
  * `AWS::Lambda::Url` resource.
40539
40693
  *
40540
40694
  * Per-shape classification:
40541
- * - `AuthType === 'NONE'` + `InvokeMode !== 'RESPONSE_STREAM'` → normal route.
40695
+ * - `AuthType === 'NONE'` + `InvokeMode === 'BUFFERED'` (or unset) → normal route.
40696
+ * - `AuthType === 'NONE'` + `InvokeMode === 'RESPONSE_STREAM'` → normal route
40697
+ * dispatched via the RIE streaming protocol (the response body is a
40698
+ * JSON prelude — `{statusCode, headers, cookies?}` — followed by 8
40699
+ * NULL bytes and then the raw body chunks). The HTTP server pipes
40700
+ * the chunks to the client with `Transfer-Encoding: chunked` (#467).
40542
40701
  * - `AuthType !== 'NONE'` (e.g. `AWS_IAM`) → deferred-error
40543
40702
  * unsupported. Boot proceeds; HTTP 501 + `reason` at request time.
40544
40703
  * IAM auth would need SigV4 verification cdkd cannot emulate.
40545
- * - `InvokeMode === 'RESPONSE_STREAM'` → deferred-error unsupported.
40546
- * The RIE container does not implement `InvokeWithResponseStream`.
40547
40704
  *
40548
40705
  * The Lambda Arn intrinsic resolution still **hard-errors** when it
40549
40706
  * cannot pin down a same-template Lambda — Function URLs have no other
@@ -40573,14 +40730,18 @@ function discoverFunctionUrl(logicalId, resource, template, stackName) {
40573
40730
  lambdaLogicalId,
40574
40731
  unsupported: { reason: `${stackName}/${logicalId}: AuthType '${String(authType)}' is not supported (only NONE — IAM auth requires SigV4 verification cdkd cannot emulate locally).` }
40575
40732
  }];
40576
- if (props["InvokeMode"] === "RESPONSE_STREAM") return [{
40733
+ const invokeModeRaw = props["InvokeMode"];
40734
+ let invokeMode = "BUFFERED";
40735
+ if (invokeModeRaw === "RESPONSE_STREAM") invokeMode = "RESPONSE_STREAM";
40736
+ else if (invokeModeRaw !== void 0 && invokeModeRaw !== "BUFFERED") return [{
40577
40737
  ...baseRoute,
40578
40738
  lambdaLogicalId,
40579
- unsupported: { reason: `${stackName}/${logicalId}: InvokeMode RESPONSE_STREAM is not supported (cdkd's RIE container does not implement InvokeWithResponseStream).` }
40739
+ unsupported: { reason: `${stackName}/${logicalId}: InvokeMode ${shortJson$1(invokeModeRaw)} is not a recognized value (expected 'BUFFERED' or 'RESPONSE_STREAM').` }
40580
40740
  }];
40581
40741
  return [{
40582
40742
  ...baseRoute,
40583
- lambdaLogicalId
40743
+ lambdaLogicalId,
40744
+ invokeMode
40584
40745
  }];
40585
40746
  }
40586
40747
  /**
@@ -43717,6 +43878,26 @@ async function handleRequest(req, res, state, opts) {
43717
43878
  writeError(res, 502);
43718
43879
  return;
43719
43880
  }
43881
+ if (match.route.invokeMode === "RESPONSE_STREAM") {
43882
+ let streamResult;
43883
+ try {
43884
+ streamResult = await invokeRieStreaming(handle.containerHost, handle.hostPort, baseEvent, opts.rieTimeoutMs);
43885
+ try {
43886
+ writeStreamingResponse(res, streamResult, () => state.pool.release(handle));
43887
+ } catch (writeErr) {
43888
+ streamResult.body.on("error", () => {});
43889
+ streamResult.body.destroy(writeErr instanceof Error ? writeErr : new Error(String(writeErr)));
43890
+ throw writeErr;
43891
+ }
43892
+ return;
43893
+ } catch (err) {
43894
+ logger.error(`RIE streaming invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
43895
+ if (!res.headersSent) writeError(res, 502);
43896
+ else res.end();
43897
+ state.pool.release(handle);
43898
+ return;
43899
+ }
43900
+ }
43720
43901
  try {
43721
43902
  const translated = translateLambdaResponse((await invokeRie(handle.containerHost, handle.hostPort, baseEvent, opts.rieTimeoutMs)).payload, match.route.apiVersion);
43722
43903
  res.statusCode = translated.statusCode;
@@ -43732,6 +43913,42 @@ async function handleRequest(req, res, state, opts) {
43732
43913
  }
43733
43914
  }
43734
43915
  /**
43916
+ * Pipe a streaming RIE response into an `http.ServerResponse`. The
43917
+ * prelude's status + headers are written via `res.writeHead(...)`; the
43918
+ * body Readable is `pipe`'d through to the response so Node's
43919
+ * `Transfer-Encoding: chunked` machinery handles backpressure +
43920
+ * chunked framing automatically.
43921
+ *
43922
+ * `releasePool` runs in a `finally`-equivalent path so the warm
43923
+ * container is returned to the pool whether the stream ends cleanly
43924
+ * (`'end'` event) or errors mid-body (`'error'` event). Errors after
43925
+ * the prelude has been written can no longer be reported as HTTP
43926
+ * status — the stream is destroyed and the connection aborts.
43927
+ *
43928
+ * Cookies in the prelude are emitted as multiple `Set-Cookie` headers
43929
+ * (HTTP API v2 semantics — matching the buffered path's behavior).
43930
+ */
43931
+ function writeStreamingResponse(res, result, releasePool) {
43932
+ const logger = getLogger().child("start-api");
43933
+ const { prelude, body } = result;
43934
+ const headersOut = { ...prelude.headers };
43935
+ if (prelude.cookies && prelude.cookies.length > 0) headersOut["set-cookie"] = prelude.cookies;
43936
+ res.writeHead(prelude.statusCode, headersOut);
43937
+ let released = false;
43938
+ const releaseOnce = () => {
43939
+ if (released) return;
43940
+ released = true;
43941
+ releasePool();
43942
+ };
43943
+ body.on("error", (err) => {
43944
+ logger.error(`Streaming Lambda response body errored mid-stream: ${err instanceof Error ? err.message : String(err)}`);
43945
+ res.destroy(err);
43946
+ releaseOnce();
43947
+ });
43948
+ res.on("close", releaseOnce);
43949
+ body.pipe(res);
43950
+ }
43951
+ /**
43735
43952
  * Attempt CORS preflight interception. Returns `true` when the
43736
43953
  * preflight response was written (caller must NOT continue to route
43737
43954
  * dispatch); `false` when no preflight match (caller falls through to
@@ -49094,7 +49311,7 @@ function reorderArgs(argv) {
49094
49311
  */
49095
49312
  async function main() {
49096
49313
  const program = new Command();
49097
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.129.0");
49314
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.130.0");
49098
49315
  program.addCommand(createBootstrapCommand());
49099
49316
  program.addCommand(createSynthCommand());
49100
49317
  program.addCommand(createListCommand());