@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 +247 -81
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
39529
|
-
*
|
|
39530
|
-
*
|
|
39531
|
-
*
|
|
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 >
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
41848
|
+
let jsonBodyParseError = false;
|
|
41849
|
+
function lazyJson(opts) {
|
|
41767
41850
|
if (!jsonBodyParsed) {
|
|
41768
41851
|
jsonBodyParsed = true;
|
|
41769
|
-
|
|
41770
|
-
|
|
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"
|
|
41920
|
-
|
|
41921
|
-
|
|
41922
|
-
|
|
41923
|
-
|
|
41924
|
-
|
|
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
|
|
42236
|
-
|
|
42237
|
-
|
|
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:
|
|
42240
|
-
headers
|
|
42241
|
-
body
|
|
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
|
-
|
|
42317
|
-
|
|
42318
|
-
|
|
42319
|
-
|
|
42320
|
-
|
|
42321
|
-
|
|
42322
|
-
|
|
42323
|
-
|
|
42324
|
-
}
|
|
42325
|
-
}
|
|
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"
|
|
42332
|
-
|
|
42333
|
-
|
|
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 = {
|
|
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.
|
|
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());
|