@go-to-k/cdkd 0.127.0 → 0.129.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/dist/cli.js +1013 -48
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -36602,7 +36602,7 @@ function extractLambdaProperties(stack, logicalId, resource, resources) {
|
|
|
36602
36602
|
const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
|
|
36603
36603
|
const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
|
|
36604
36604
|
const code = props["Code"] ?? {};
|
|
36605
|
-
const imageUri = extractImageUri(code["ImageUri"], logicalId, stack.stackName, resources);
|
|
36605
|
+
const imageUri = extractImageUri$1(code["ImageUri"], logicalId, stack.stackName, resources);
|
|
36606
36606
|
if (imageUri !== void 0) return extractImageLambdaProperties({
|
|
36607
36607
|
stack,
|
|
36608
36608
|
logicalId,
|
|
@@ -36699,7 +36699,7 @@ function extractEphemeralStorageMb(props, logicalId) {
|
|
|
36699
36699
|
* for genuinely unrecognized shapes so the caller's downstream ZIP-vs-
|
|
36700
36700
|
* IMAGE branching can route to its existing error path.
|
|
36701
36701
|
*/
|
|
36702
|
-
function extractImageUri(value, logicalId, stackName, resources) {
|
|
36702
|
+
function extractImageUri$1(value, logicalId, stackName, resources) {
|
|
36703
36703
|
if (typeof value === "string" && value.length > 0) return value;
|
|
36704
36704
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
36705
36705
|
const obj = value;
|
|
@@ -39795,6 +39795,398 @@ function resolveFnSubInvokeArn(arg) {
|
|
|
39795
39795
|
};
|
|
39796
39796
|
}
|
|
39797
39797
|
|
|
39798
|
+
//#endregion
|
|
39799
|
+
//#region src/local/httpv2-service-integration.ts
|
|
39800
|
+
const logger = getLogger();
|
|
39801
|
+
/**
|
|
39802
|
+
* Full list of subtypes cdkd recognizes as supported. Mirrors AWS docs.
|
|
39803
|
+
*/
|
|
39804
|
+
const SUPPORTED_SUBTYPES = [
|
|
39805
|
+
"EventBridge-PutEvents",
|
|
39806
|
+
"SQS-SendMessage",
|
|
39807
|
+
"SQS-ReceiveMessage",
|
|
39808
|
+
"SQS-DeleteMessage",
|
|
39809
|
+
"SQS-PurgeQueue",
|
|
39810
|
+
"Kinesis-PutRecord",
|
|
39811
|
+
"StepFunctions-StartExecution",
|
|
39812
|
+
"StepFunctions-StartSyncExecution",
|
|
39813
|
+
"StepFunctions-StopExecution",
|
|
39814
|
+
"AppConfig-GetConfiguration"
|
|
39815
|
+
];
|
|
39816
|
+
/**
|
|
39817
|
+
* Type guard: is the string an AWS-recognized service-integration subtype?
|
|
39818
|
+
*
|
|
39819
|
+
* Used by route discovery to classify routes — recognized subtypes go
|
|
39820
|
+
* through dispatch, anything else (typo, future-AWS-subtype-not-yet-supported)
|
|
39821
|
+
* falls back to deferred-501.
|
|
39822
|
+
*/
|
|
39823
|
+
function isSupportedSubtype(value) {
|
|
39824
|
+
return typeof value === "string" && SUPPORTED_SUBTYPES.includes(value);
|
|
39825
|
+
}
|
|
39826
|
+
/**
|
|
39827
|
+
* Lazy-loaded SDK clients keyed by `<service>:<region>`. SDK packages
|
|
39828
|
+
* are heavyweight (~5-10 MB each); per-process per-region caching
|
|
39829
|
+
* avoids re-instantiating the AWS Signer + middleware stack on every
|
|
39830
|
+
* request.
|
|
39831
|
+
*/
|
|
39832
|
+
const clientCache = /* @__PURE__ */ new Map();
|
|
39833
|
+
async function getClient(service, region) {
|
|
39834
|
+
const key = `${service}:${region}`;
|
|
39835
|
+
const cached = clientCache.get(key);
|
|
39836
|
+
if (cached) return cached;
|
|
39837
|
+
let client;
|
|
39838
|
+
switch (service) {
|
|
39839
|
+
case "sqs":
|
|
39840
|
+
client = new (await (import("@aws-sdk/client-sqs"))).SQSClient({ region });
|
|
39841
|
+
break;
|
|
39842
|
+
case "sns":
|
|
39843
|
+
client = new (await (import("@aws-sdk/client-sns"))).SNSClient({ region });
|
|
39844
|
+
break;
|
|
39845
|
+
case "eventbridge":
|
|
39846
|
+
client = new (await (import("@aws-sdk/client-eventbridge"))).EventBridgeClient({ region });
|
|
39847
|
+
break;
|
|
39848
|
+
case "kinesis":
|
|
39849
|
+
client = new (await (import("@aws-sdk/client-kinesis"))).KinesisClient({ region });
|
|
39850
|
+
break;
|
|
39851
|
+
case "sfn":
|
|
39852
|
+
client = new (await (import("@aws-sdk/client-sfn"))).SFNClient({ region });
|
|
39853
|
+
break;
|
|
39854
|
+
case "ssm":
|
|
39855
|
+
client = new (await (import("@aws-sdk/client-ssm"))).SSMClient({ region });
|
|
39856
|
+
break;
|
|
39857
|
+
default: throw new Error(`unknown service '${service}'`);
|
|
39858
|
+
}
|
|
39859
|
+
clientCache.set(key, client);
|
|
39860
|
+
return client;
|
|
39861
|
+
}
|
|
39862
|
+
/**
|
|
39863
|
+
* Dispatch a service integration: build the SDK input from the
|
|
39864
|
+
* pre-resolved parameter map, invoke the SDK, translate the response
|
|
39865
|
+
* to HTTP shape.
|
|
39866
|
+
*
|
|
39867
|
+
* `defaultRegion` is the cdkd process's default AWS region (from
|
|
39868
|
+
* `AWS_REGION` / profile / `--region`). When the resolved parameter
|
|
39869
|
+
* map includes a non-empty `Region`, that value overrides the default
|
|
39870
|
+
* for this single call — matches AWS API Gateway behavior.
|
|
39871
|
+
*
|
|
39872
|
+
* Returns a `ServiceIntegrationResult` for the HTTP server to write
|
|
39873
|
+
* to the client. SDK-level errors are caught and translated to
|
|
39874
|
+
* HTTP 4xx / 5xx — never thrown.
|
|
39875
|
+
*/
|
|
39876
|
+
async function dispatchServiceIntegration(subtype, resolvedParameters, defaultRegion) {
|
|
39877
|
+
const region = (resolvedParameters["Region"] || defaultRegion).trim();
|
|
39878
|
+
if (!region) return errorResponse(400, "No AWS region configured. Set --region, AWS_REGION, or pass a 'Region' RequestParameter.");
|
|
39879
|
+
try {
|
|
39880
|
+
switch (subtype) {
|
|
39881
|
+
case "EventBridge-PutEvents": return await dispatchEventBridgePutEvents(resolvedParameters, region);
|
|
39882
|
+
case "SQS-SendMessage": return await dispatchSqsSendMessage(resolvedParameters, region);
|
|
39883
|
+
case "SQS-ReceiveMessage": return await dispatchSqsReceiveMessage(resolvedParameters, region);
|
|
39884
|
+
case "SQS-DeleteMessage": return await dispatchSqsDeleteMessage(resolvedParameters, region);
|
|
39885
|
+
case "SQS-PurgeQueue": return await dispatchSqsPurgeQueue(resolvedParameters, region);
|
|
39886
|
+
case "Kinesis-PutRecord": return await dispatchKinesisPutRecord(resolvedParameters, region);
|
|
39887
|
+
case "StepFunctions-StartExecution": return await dispatchSfnStartExecution(resolvedParameters, region);
|
|
39888
|
+
case "StepFunctions-StartSyncExecution": return await dispatchSfnStartSyncExecution(resolvedParameters, region);
|
|
39889
|
+
case "StepFunctions-StopExecution": return await dispatchSfnStopExecution(resolvedParameters, region);
|
|
39890
|
+
case "AppConfig-GetConfiguration": return await dispatchAppConfigGetConfiguration(resolvedParameters, region);
|
|
39891
|
+
}
|
|
39892
|
+
} catch (err) {
|
|
39893
|
+
return translateSdkError(subtype, err);
|
|
39894
|
+
}
|
|
39895
|
+
}
|
|
39896
|
+
async function dispatchEventBridgePutEvents(params, region) {
|
|
39897
|
+
requireParams(params, [
|
|
39898
|
+
"Detail",
|
|
39899
|
+
"DetailType",
|
|
39900
|
+
"Source"
|
|
39901
|
+
]);
|
|
39902
|
+
const mod = await import("@aws-sdk/client-eventbridge");
|
|
39903
|
+
const client = await getClient("eventbridge", region);
|
|
39904
|
+
const entry = {
|
|
39905
|
+
Detail: params["Detail"],
|
|
39906
|
+
DetailType: params["DetailType"],
|
|
39907
|
+
Source: params["Source"]
|
|
39908
|
+
};
|
|
39909
|
+
if (params["Time"]) entry["Time"] = new Date(params["Time"]);
|
|
39910
|
+
if (params["EventBusName"]) entry["EventBusName"] = params["EventBusName"];
|
|
39911
|
+
if (params["Resources"]) entry["Resources"] = splitCsv(params["Resources"]);
|
|
39912
|
+
if (params["TraceHeader"]) entry["TraceHeader"] = params["TraceHeader"];
|
|
39913
|
+
return okJson(await client.send(new mod.PutEventsCommand({ Entries: [entry] })));
|
|
39914
|
+
}
|
|
39915
|
+
async function dispatchSqsSendMessage(params, region) {
|
|
39916
|
+
requireParams(params, ["QueueUrl", "MessageBody"]);
|
|
39917
|
+
const mod = await import("@aws-sdk/client-sqs");
|
|
39918
|
+
const client = await getClient("sqs", region);
|
|
39919
|
+
const input = {
|
|
39920
|
+
QueueUrl: params["QueueUrl"],
|
|
39921
|
+
MessageBody: params["MessageBody"]
|
|
39922
|
+
};
|
|
39923
|
+
if (params["DelaySeconds"]) input["DelaySeconds"] = Number(params["DelaySeconds"]);
|
|
39924
|
+
if (params["MessageDeduplicationId"]) input["MessageDeduplicationId"] = params["MessageDeduplicationId"];
|
|
39925
|
+
if (params["MessageGroupId"]) input["MessageGroupId"] = params["MessageGroupId"];
|
|
39926
|
+
if (params["MessageAttributes"]) input["MessageAttributes"] = parseJsonOrEmpty(params["MessageAttributes"]);
|
|
39927
|
+
if (params["MessageSystemAttributes"]) input["MessageSystemAttributes"] = parseJsonOrEmpty(params["MessageSystemAttributes"]);
|
|
39928
|
+
return okJson(await client.send(new mod.SendMessageCommand(input)));
|
|
39929
|
+
}
|
|
39930
|
+
async function dispatchSqsReceiveMessage(params, region) {
|
|
39931
|
+
requireParams(params, ["QueueUrl"]);
|
|
39932
|
+
const mod = await import("@aws-sdk/client-sqs");
|
|
39933
|
+
const client = await getClient("sqs", region);
|
|
39934
|
+
const input = { QueueUrl: params["QueueUrl"] };
|
|
39935
|
+
if (params["AttributeNames"]) input["AttributeNames"] = splitCsv(params["AttributeNames"]);
|
|
39936
|
+
if (params["MaxNumberOfMessages"]) input["MaxNumberOfMessages"] = Number(params["MaxNumberOfMessages"]);
|
|
39937
|
+
if (params["MessageAttributeNames"]) input["MessageAttributeNames"] = splitCsv(params["MessageAttributeNames"]);
|
|
39938
|
+
if (params["ReceiveRequestAttemptId"]) input["ReceiveRequestAttemptId"] = params["ReceiveRequestAttemptId"];
|
|
39939
|
+
if (params["VisibilityTimeout"]) input["VisibilityTimeout"] = Number(params["VisibilityTimeout"]);
|
|
39940
|
+
if (params["WaitTimeSeconds"]) input["WaitTimeSeconds"] = Number(params["WaitTimeSeconds"]);
|
|
39941
|
+
return okJson(await client.send(new mod.ReceiveMessageCommand(input)));
|
|
39942
|
+
}
|
|
39943
|
+
async function dispatchSqsDeleteMessage(params, region) {
|
|
39944
|
+
requireParams(params, ["QueueUrl", "ReceiptHandle"]);
|
|
39945
|
+
const mod = await import("@aws-sdk/client-sqs");
|
|
39946
|
+
return okJson(await (await getClient("sqs", region)).send(new mod.DeleteMessageCommand({
|
|
39947
|
+
QueueUrl: params["QueueUrl"],
|
|
39948
|
+
ReceiptHandle: params["ReceiptHandle"]
|
|
39949
|
+
})));
|
|
39950
|
+
}
|
|
39951
|
+
async function dispatchSqsPurgeQueue(params, region) {
|
|
39952
|
+
requireParams(params, ["QueueUrl"]);
|
|
39953
|
+
const mod = await import("@aws-sdk/client-sqs");
|
|
39954
|
+
return okJson(await (await getClient("sqs", region)).send(new mod.PurgeQueueCommand({ QueueUrl: params["QueueUrl"] })));
|
|
39955
|
+
}
|
|
39956
|
+
async function dispatchKinesisPutRecord(params, region) {
|
|
39957
|
+
requireParams(params, [
|
|
39958
|
+
"StreamName",
|
|
39959
|
+
"Data",
|
|
39960
|
+
"PartitionKey"
|
|
39961
|
+
]);
|
|
39962
|
+
const mod = await import("@aws-sdk/client-kinesis");
|
|
39963
|
+
const client = await getClient("kinesis", region);
|
|
39964
|
+
const dataBytes = decodeBase64OrUtf8(params["Data"] ?? "");
|
|
39965
|
+
const input = {
|
|
39966
|
+
StreamName: params["StreamName"],
|
|
39967
|
+
Data: dataBytes,
|
|
39968
|
+
PartitionKey: params["PartitionKey"]
|
|
39969
|
+
};
|
|
39970
|
+
if (params["SequenceNumberForOrdering"]) input["SequenceNumberForOrdering"] = params["SequenceNumberForOrdering"];
|
|
39971
|
+
if (params["ExplicitHashKey"]) input["ExplicitHashKey"] = params["ExplicitHashKey"];
|
|
39972
|
+
return okJson(await client.send(new mod.PutRecordCommand(input)));
|
|
39973
|
+
}
|
|
39974
|
+
async function dispatchSfnStartExecution(params, region) {
|
|
39975
|
+
requireParams(params, ["StateMachineArn"]);
|
|
39976
|
+
const mod = await import("@aws-sdk/client-sfn");
|
|
39977
|
+
const client = await getClient("sfn", region);
|
|
39978
|
+
const input = { stateMachineArn: params["StateMachineArn"] };
|
|
39979
|
+
if (params["Name"]) input["name"] = params["Name"];
|
|
39980
|
+
if (params["Input"]) input["input"] = params["Input"];
|
|
39981
|
+
return okJson(await client.send(new mod.StartExecutionCommand(input)));
|
|
39982
|
+
}
|
|
39983
|
+
async function dispatchSfnStartSyncExecution(params, region) {
|
|
39984
|
+
requireParams(params, ["StateMachineArn"]);
|
|
39985
|
+
const mod = await import("@aws-sdk/client-sfn");
|
|
39986
|
+
const client = await getClient("sfn", region);
|
|
39987
|
+
const input = { stateMachineArn: params["StateMachineArn"] };
|
|
39988
|
+
if (params["Name"]) input["name"] = params["Name"];
|
|
39989
|
+
if (params["Input"]) input["input"] = params["Input"];
|
|
39990
|
+
if (params["TraceHeader"]) input["traceHeader"] = params["TraceHeader"];
|
|
39991
|
+
return okJson(await client.send(new mod.StartSyncExecutionCommand(input)));
|
|
39992
|
+
}
|
|
39993
|
+
async function dispatchSfnStopExecution(params, region) {
|
|
39994
|
+
requireParams(params, ["ExecutionArn"]);
|
|
39995
|
+
const mod = await import("@aws-sdk/client-sfn");
|
|
39996
|
+
const client = await getClient("sfn", region);
|
|
39997
|
+
const input = { executionArn: params["ExecutionArn"] };
|
|
39998
|
+
if (params["Cause"]) input["cause"] = params["Cause"];
|
|
39999
|
+
if (params["Error"]) input["error"] = params["Error"];
|
|
40000
|
+
return okJson(await client.send(new mod.StopExecutionCommand(input)));
|
|
40001
|
+
}
|
|
40002
|
+
async function dispatchAppConfigGetConfiguration(params, region) {
|
|
40003
|
+
return errorResponse(501, "AppConfig-GetConfiguration is recognized but cdkd does not yet bundle @aws-sdk/client-appconfig. Use the deployed API for this subtype, or open an issue if you need local emulation.");
|
|
40004
|
+
}
|
|
40005
|
+
function requireParams(params, required) {
|
|
40006
|
+
const missing = required.filter((k) => !params[k] || params[k].trim() === "");
|
|
40007
|
+
if (missing.length > 0) {
|
|
40008
|
+
const err = /* @__PURE__ */ new Error(`missing required RequestParameter(s): ${missing.join(", ")}`);
|
|
40009
|
+
err.statusCode = 400;
|
|
40010
|
+
throw err;
|
|
40011
|
+
}
|
|
40012
|
+
}
|
|
40013
|
+
function splitCsv(value) {
|
|
40014
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
40015
|
+
}
|
|
40016
|
+
function parseJsonOrEmpty(value) {
|
|
40017
|
+
try {
|
|
40018
|
+
const parsed = JSON.parse(value);
|
|
40019
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
40020
|
+
return {};
|
|
40021
|
+
} catch {
|
|
40022
|
+
return {};
|
|
40023
|
+
}
|
|
40024
|
+
}
|
|
40025
|
+
function decodeBase64OrUtf8(value) {
|
|
40026
|
+
const trimmed = value.trim();
|
|
40027
|
+
if (/^[A-Za-z0-9+/]+=*$/.test(trimmed) && trimmed.length % 4 === 0 && trimmed.length > 0) try {
|
|
40028
|
+
return Buffer.from(trimmed, "base64");
|
|
40029
|
+
} catch {}
|
|
40030
|
+
return Buffer.from(value, "utf8");
|
|
40031
|
+
}
|
|
40032
|
+
function okJson(response) {
|
|
40033
|
+
const stripped = stripSdkMetadata(response);
|
|
40034
|
+
return {
|
|
40035
|
+
statusCode: 200,
|
|
40036
|
+
body: JSON.stringify(stripped),
|
|
40037
|
+
headers: { "content-type": "application/json" }
|
|
40038
|
+
};
|
|
40039
|
+
}
|
|
40040
|
+
function stripSdkMetadata(obj) {
|
|
40041
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") return obj;
|
|
40042
|
+
if (Array.isArray(obj)) return obj;
|
|
40043
|
+
const { $metadata: _meta, ...rest } = obj;
|
|
40044
|
+
return rest;
|
|
40045
|
+
}
|
|
40046
|
+
function errorResponse(statusCode, message) {
|
|
40047
|
+
return {
|
|
40048
|
+
statusCode,
|
|
40049
|
+
body: JSON.stringify({ message }),
|
|
40050
|
+
headers: { "content-type": "application/json" }
|
|
40051
|
+
};
|
|
40052
|
+
}
|
|
40053
|
+
/**
|
|
40054
|
+
* Translate an AWS SDK error to an HTTP response. AWS SDK v3 surfaces
|
|
40055
|
+
* errors as instances carrying `$metadata.httpStatusCode` + `name`;
|
|
40056
|
+
* we honor the status code when present, default to 500.
|
|
40057
|
+
*/
|
|
40058
|
+
function translateSdkError(subtype, err) {
|
|
40059
|
+
if (err && typeof err === "object") {
|
|
40060
|
+
const e = err;
|
|
40061
|
+
const status = typeof e.statusCode === "number" && e.statusCode >= 100 && e.statusCode < 600 ? e.statusCode : e.$metadata?.httpStatusCode ?? 500;
|
|
40062
|
+
const body = {
|
|
40063
|
+
message: e.message ?? "AWS SDK call failed",
|
|
40064
|
+
code: e.name ?? "UnknownError"
|
|
40065
|
+
};
|
|
40066
|
+
logger.debug(`[${subtype}] SDK error (${status}): ${stringifyValue(body)}`);
|
|
40067
|
+
return {
|
|
40068
|
+
statusCode: status,
|
|
40069
|
+
body: JSON.stringify(body),
|
|
40070
|
+
headers: { "content-type": "application/json" }
|
|
40071
|
+
};
|
|
40072
|
+
}
|
|
40073
|
+
return errorResponse(500, `Unexpected error invoking ${subtype}: ${String(err)}`);
|
|
40074
|
+
}
|
|
40075
|
+
/**
|
|
40076
|
+
* Apply HTTP API v2 `ResponseParameters` mapping (per AWS docs:
|
|
40077
|
+
* https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html#http-api-mapping-supported-values).
|
|
40078
|
+
*
|
|
40079
|
+
* Keys are `<op>:header.<name>` or `overwrite:statuscode`. Values can
|
|
40080
|
+
* carry `$response.header.<name>`, `$context.<X>`, `$stageVariables.<X>`,
|
|
40081
|
+
* or be a static literal. JSONPath against `$response.body.X` is NOT
|
|
40082
|
+
* supported (would require SDK response parsing into the same shape
|
|
40083
|
+
* every subtype produces; deferred). Reserved headers (per AWS docs)
|
|
40084
|
+
* are rejected at this layer with a single-line debug log.
|
|
40085
|
+
*
|
|
40086
|
+
* Status-code lookup is by the SDK-returned `statusCode`. When the
|
|
40087
|
+
* exact code has no entry, the wildcard `'default'` entry is applied
|
|
40088
|
+
* if present (matches AWS deployed behavior).
|
|
40089
|
+
*/
|
|
40090
|
+
function applyResponseParameters(base, responseParameters, responseCtx) {
|
|
40091
|
+
if (!responseParameters) return base;
|
|
40092
|
+
const overlay = responseParameters[String(base.statusCode)] ?? responseParameters["default"] ?? void 0;
|
|
40093
|
+
if (!overlay) return base;
|
|
40094
|
+
let statusCode = base.statusCode;
|
|
40095
|
+
const headers = { ...base.headers };
|
|
40096
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
40097
|
+
if (typeof value !== "string") continue;
|
|
40098
|
+
const resolved = resolveResponseValue(value, responseCtx, base);
|
|
40099
|
+
if (key === "overwrite:statuscode") {
|
|
40100
|
+
const next = Number(resolved);
|
|
40101
|
+
if (Number.isInteger(next) && next >= 100 && next < 600) statusCode = next;
|
|
40102
|
+
continue;
|
|
40103
|
+
}
|
|
40104
|
+
const headerMatch = /^(append|overwrite|remove):header\.(.+)$/i.exec(key);
|
|
40105
|
+
if (!headerMatch || !headerMatch[1] || !headerMatch[2]) continue;
|
|
40106
|
+
const op = headerMatch[1].toLowerCase();
|
|
40107
|
+
const name = headerMatch[2].toLowerCase();
|
|
40108
|
+
if (isReservedHeader(name)) {
|
|
40109
|
+
logger.debug(`ResponseParameters: header '${name}' is reserved by API Gateway and was skipped`);
|
|
40110
|
+
continue;
|
|
40111
|
+
}
|
|
40112
|
+
if (op === "remove") delete headers[name];
|
|
40113
|
+
else if (op === "overwrite") headers[name] = resolved;
|
|
40114
|
+
else if (op === "append") headers[name] = headers[name] ? `${headers[name]},${resolved}` : resolved;
|
|
40115
|
+
}
|
|
40116
|
+
return {
|
|
40117
|
+
statusCode,
|
|
40118
|
+
body: base.body,
|
|
40119
|
+
headers
|
|
40120
|
+
};
|
|
40121
|
+
}
|
|
40122
|
+
function resolveResponseValue(value, ctx, base) {
|
|
40123
|
+
if (value.startsWith("$") && !value.includes("${")) {
|
|
40124
|
+
const r = resolveSingleResponseRef(value, ctx, base);
|
|
40125
|
+
return r !== void 0 ? r : value;
|
|
40126
|
+
}
|
|
40127
|
+
if (value.includes("${")) {
|
|
40128
|
+
let out = "";
|
|
40129
|
+
let i = 0;
|
|
40130
|
+
while (i < value.length) {
|
|
40131
|
+
const next = value.indexOf("${", i);
|
|
40132
|
+
if (next === -1) {
|
|
40133
|
+
out += value.slice(i);
|
|
40134
|
+
break;
|
|
40135
|
+
}
|
|
40136
|
+
out += value.slice(i, next);
|
|
40137
|
+
const end = value.indexOf("}", next + 2);
|
|
40138
|
+
if (end === -1) return value;
|
|
40139
|
+
const r = resolveSingleResponseRef("$" + value.slice(next + 2, end), ctx, base);
|
|
40140
|
+
out += r ?? "";
|
|
40141
|
+
i = end + 1;
|
|
40142
|
+
}
|
|
40143
|
+
return out;
|
|
40144
|
+
}
|
|
40145
|
+
return value;
|
|
40146
|
+
}
|
|
40147
|
+
function resolveSingleResponseRef(ref, ctx, base) {
|
|
40148
|
+
if (ref.startsWith("$response.header.")) {
|
|
40149
|
+
const name = ref.substring(17).toLowerCase();
|
|
40150
|
+
return base.headers[name] ?? "";
|
|
40151
|
+
}
|
|
40152
|
+
if (ref.startsWith("$context.")) return ctx.context[ref.substring(9)] ?? "";
|
|
40153
|
+
if (ref.startsWith("$stageVariables.")) return ctx.stageVariables[ref.substring(16)] ?? "";
|
|
40154
|
+
}
|
|
40155
|
+
/**
|
|
40156
|
+
* Subset of AWS's reserved-headers list relevant to response mapping.
|
|
40157
|
+
* https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html#http-api-mapping-reserved-headers
|
|
40158
|
+
*/
|
|
40159
|
+
const RESERVED_HEADER_PREFIXES = [
|
|
40160
|
+
"access-control-",
|
|
40161
|
+
"apigw-",
|
|
40162
|
+
"x-amz-",
|
|
40163
|
+
"x-amzn-"
|
|
40164
|
+
];
|
|
40165
|
+
const RESERVED_HEADER_EXACT = [
|
|
40166
|
+
"authorization",
|
|
40167
|
+
"connection",
|
|
40168
|
+
"content-encoding",
|
|
40169
|
+
"content-length",
|
|
40170
|
+
"content-location",
|
|
40171
|
+
"forwarded",
|
|
40172
|
+
"keep-alive",
|
|
40173
|
+
"origin",
|
|
40174
|
+
"proxy-authenticate",
|
|
40175
|
+
"proxy-authorization",
|
|
40176
|
+
"te",
|
|
40177
|
+
"trailers",
|
|
40178
|
+
"transfer-encoding",
|
|
40179
|
+
"upgrade",
|
|
40180
|
+
"x-forwarded-for",
|
|
40181
|
+
"x-forwarded-host",
|
|
40182
|
+
"x-forwarded-proto",
|
|
40183
|
+
"via"
|
|
40184
|
+
];
|
|
40185
|
+
function isReservedHeader(lowerName) {
|
|
40186
|
+
if (RESERVED_HEADER_EXACT.includes(lowerName)) return true;
|
|
40187
|
+
return RESERVED_HEADER_PREFIXES.some((p) => lowerName.startsWith(p));
|
|
40188
|
+
}
|
|
40189
|
+
|
|
39798
40190
|
//#endregion
|
|
39799
40191
|
//#region src/local/route-discovery.ts
|
|
39800
40192
|
/**
|
|
@@ -40090,11 +40482,7 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
|
|
|
40090
40482
|
lambdaLogicalId: "",
|
|
40091
40483
|
unsupported: { reason: `${stackName}/${logicalId}: HTTP API v2 integration type '${String(integrationType)}' is not supported (only AWS_PROXY).` }
|
|
40092
40484
|
}];
|
|
40093
|
-
if (integrationProps["IntegrationSubtype"] !== void 0) return [
|
|
40094
|
-
...baseRoute,
|
|
40095
|
-
lambdaLogicalId: "",
|
|
40096
|
-
unsupported: { reason: `${stackName}/${logicalId}: HTTP API v2 service integration with IntegrationSubtype '${stringifyValue(integrationProps["IntegrationSubtype"])}' is not supported (cdkd cannot proxy directly to SQS / EventBridge / etc.).` }
|
|
40097
|
-
}];
|
|
40485
|
+
if (integrationProps["IntegrationSubtype"] !== void 0) return [classifyServiceIntegrationRoute(baseRoute, integrationProps, stackName, logicalId, integrationLogicalId)];
|
|
40098
40486
|
const arnOutcome = resolveLambdaArnOutcome(integrationProps["IntegrationUri"]);
|
|
40099
40487
|
if (arnOutcome.kind === "unsupported") return [{
|
|
40100
40488
|
...baseRoute,
|
|
@@ -40107,6 +40495,45 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
|
|
|
40107
40495
|
}];
|
|
40108
40496
|
}
|
|
40109
40497
|
/**
|
|
40498
|
+
* Classify an HTTP API v2 route whose `IntegrationSubtype` is set
|
|
40499
|
+
* (service integration, no Lambda backing). Recognized subtypes
|
|
40500
|
+
* become `serviceIntegration` routes the HTTP server dispatches via
|
|
40501
|
+
* the SDK adapter table in `httpv2-service-integration.ts`;
|
|
40502
|
+
* unrecognized subtypes (typo / future-AWS-subtype-cdkd-doesn't-bundle
|
|
40503
|
+
* an SDK for) become deferred-501 unsupported routes.
|
|
40504
|
+
*
|
|
40505
|
+
* `RequestParameters` is the load-bearing field for dispatch — it
|
|
40506
|
+
* carries the SDK input as a flat map keyed by SDK parameter name.
|
|
40507
|
+
* Missing / non-object RequestParameters surfaces as unsupported (the
|
|
40508
|
+
* SDK call would have nothing to send).
|
|
40509
|
+
*/
|
|
40510
|
+
function classifyServiceIntegrationRoute(baseRoute, integrationProps, stackName, routeLogicalId, integrationLogicalId) {
|
|
40511
|
+
const subtypeRaw = integrationProps["IntegrationSubtype"];
|
|
40512
|
+
const declaredAt = `${stackName}/${routeLogicalId}`;
|
|
40513
|
+
if (!isSupportedSubtype(subtypeRaw)) return {
|
|
40514
|
+
...baseRoute,
|
|
40515
|
+
lambdaLogicalId: "",
|
|
40516
|
+
unsupported: { reason: `${declaredAt}: HTTP API v2 service integration subtype '${stringifyValue(subtypeRaw)}' is not supported by cdkd local start-api (see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html for the supported list).` }
|
|
40517
|
+
};
|
|
40518
|
+
const requestParameters = integrationProps["RequestParameters"];
|
|
40519
|
+
if (!requestParameters || typeof requestParameters !== "object" || Array.isArray(requestParameters)) return {
|
|
40520
|
+
...baseRoute,
|
|
40521
|
+
lambdaLogicalId: "",
|
|
40522
|
+
unsupported: { reason: `${stackName}/${integrationLogicalId}: HTTP API v2 service integration '${subtypeRaw}' is missing RequestParameters or it is not an object — cannot dispatch to the SDK without input mapping.` }
|
|
40523
|
+
};
|
|
40524
|
+
const responseParameters = integrationProps["ResponseParameters"];
|
|
40525
|
+
const validatedResponseParameters = responseParameters && typeof responseParameters === "object" && !Array.isArray(responseParameters) ? responseParameters : void 0;
|
|
40526
|
+
return {
|
|
40527
|
+
...baseRoute,
|
|
40528
|
+
lambdaLogicalId: "",
|
|
40529
|
+
serviceIntegration: {
|
|
40530
|
+
subtype: subtypeRaw,
|
|
40531
|
+
requestParameters,
|
|
40532
|
+
...validatedResponseParameters && { responseParameters: validatedResponseParameters }
|
|
40533
|
+
}
|
|
40534
|
+
};
|
|
40535
|
+
}
|
|
40536
|
+
/**
|
|
40110
40537
|
* Discover the synthetic `ANY /{proxy+}` route from an
|
|
40111
40538
|
* `AWS::Lambda::Url` resource.
|
|
40112
40539
|
*
|
|
@@ -40347,31 +40774,56 @@ function createContainerPool(specs, options) {
|
|
|
40347
40774
|
/**
|
|
40348
40775
|
* Spin up one new container for the given Lambda spec. Returns a
|
|
40349
40776
|
* handle the caller can write into the entry's data structures.
|
|
40777
|
+
*
|
|
40778
|
+
* Branches on `spec.kind`:
|
|
40779
|
+
* - `'zip'`: bind-mount the function's local code dir at
|
|
40780
|
+
* `/var/task` (or `/var/runtime` for `provided.*` runtimes),
|
|
40781
|
+
* base image from `public.ecr.aws/lambda/<lang>:<v>`, CMD =
|
|
40782
|
+
* `[<Handler>]`.
|
|
40783
|
+
* - `'image'`: no code bind-mount (image already includes the
|
|
40784
|
+
* code), base image is the pre-built local tag, CMD =
|
|
40785
|
+
* `ImageConfig.Command` (may be empty), optional EntryPoint /
|
|
40786
|
+
* WorkingDirectory / --platform applied verbatim.
|
|
40350
40787
|
*/
|
|
40351
40788
|
async function startOne(spec) {
|
|
40352
|
-
const image = resolveRuntimeImage(spec.lambda.runtime);
|
|
40353
40789
|
const hostPort = await pickFreePort();
|
|
40354
40790
|
const name = `cdkd-local-${spec.lambda.logicalId}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
40355
|
-
logger.debug(`Starting container ${name} for ${spec.lambda.logicalId} on ${spec.containerHost}:${hostPort}`);
|
|
40356
|
-
|
|
40357
|
-
|
|
40358
|
-
|
|
40359
|
-
|
|
40360
|
-
|
|
40361
|
-
const containerCodePath = resolveRuntimeCodeMountPath(spec.lambda.runtime);
|
|
40362
|
-
const containerId = await runDetached({
|
|
40363
|
-
image,
|
|
40364
|
-
mounts: [{
|
|
40365
|
-
hostPath: spec.codeDir,
|
|
40366
|
-
containerPath: containerCodePath,
|
|
40791
|
+
logger.debug(`Starting container ${name} for ${spec.lambda.logicalId} (kind=${spec.kind}) on ${spec.containerHost}:${hostPort}`);
|
|
40792
|
+
let containerId;
|
|
40793
|
+
if (spec.kind === "zip") {
|
|
40794
|
+
const optMount = spec.optDir ? [{
|
|
40795
|
+
hostPath: spec.optDir,
|
|
40796
|
+
containerPath: "/opt",
|
|
40367
40797
|
readOnly: true
|
|
40368
|
-
}]
|
|
40369
|
-
|
|
40798
|
+
}] : [];
|
|
40799
|
+
const containerCodePath = resolveRuntimeCodeMountPath(spec.lambda.runtime);
|
|
40800
|
+
containerId = await runDetached({
|
|
40801
|
+
image: resolveRuntimeImage(spec.lambda.runtime),
|
|
40802
|
+
mounts: [{
|
|
40803
|
+
hostPath: spec.codeDir,
|
|
40804
|
+
containerPath: containerCodePath,
|
|
40805
|
+
readOnly: true
|
|
40806
|
+
}],
|
|
40807
|
+
extraMounts: optMount,
|
|
40808
|
+
env: spec.env,
|
|
40809
|
+
cmd: [spec.lambda.handler],
|
|
40810
|
+
hostPort,
|
|
40811
|
+
host: spec.containerHost,
|
|
40812
|
+
name,
|
|
40813
|
+
...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
|
|
40814
|
+
...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
|
|
40815
|
+
});
|
|
40816
|
+
} else containerId = await runDetached({
|
|
40817
|
+
image: spec.image,
|
|
40818
|
+
mounts: [],
|
|
40370
40819
|
env: spec.env,
|
|
40371
|
-
cmd:
|
|
40820
|
+
cmd: spec.command,
|
|
40372
40821
|
hostPort,
|
|
40373
40822
|
host: spec.containerHost,
|
|
40374
40823
|
name,
|
|
40824
|
+
platform: spec.platform,
|
|
40825
|
+
...spec.entryPoint !== void 0 && { entryPoint: spec.entryPoint },
|
|
40826
|
+
...spec.workingDir !== void 0 && { workingDir: spec.workingDir },
|
|
40375
40827
|
...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
|
|
40376
40828
|
...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
|
|
40377
40829
|
});
|
|
@@ -41736,7 +42188,7 @@ function attachAuthorizers(stacks, routes) {
|
|
|
41736
42188
|
const out = [];
|
|
41737
42189
|
const errors = [];
|
|
41738
42190
|
for (const route of routes) {
|
|
41739
|
-
if (route.unsupported || route.mockCors) {
|
|
42191
|
+
if (route.unsupported || route.mockCors || route.serviceIntegration) {
|
|
41740
42192
|
out.push({ route });
|
|
41741
42193
|
continue;
|
|
41742
42194
|
}
|
|
@@ -41766,6 +42218,45 @@ function attachAuthorizers(stacks, routes) {
|
|
|
41766
42218
|
return out;
|
|
41767
42219
|
}
|
|
41768
42220
|
/**
|
|
42221
|
+
* Walk every discovered route and return the service-integration routes
|
|
42222
|
+
* whose source CFn resource declares an authorizer (HTTP API v2 routes
|
|
42223
|
+
* with `AuthorizationType !== 'NONE'`). Service-integration routes only
|
|
42224
|
+
* exist on `AWS::ApiGatewayV2::Route`; REST v1 service integrations are
|
|
42225
|
+
* a different shape and not yet supported.
|
|
42226
|
+
*/
|
|
42227
|
+
function findIgnoredServiceIntegrationAuthorizers(stacks, routes) {
|
|
42228
|
+
const stackByRoute = /* @__PURE__ */ new Map();
|
|
42229
|
+
for (const stack of stacks) {
|
|
42230
|
+
const prefix = `${stack.stackName}/`;
|
|
42231
|
+
for (const route of routes) if (route.declaredAt.startsWith(prefix)) stackByRoute.set(route.declaredAt, stack);
|
|
42232
|
+
}
|
|
42233
|
+
const out = [];
|
|
42234
|
+
for (const route of routes) {
|
|
42235
|
+
if (!route.serviceIntegration) continue;
|
|
42236
|
+
const stack = stackByRoute.get(route.declaredAt);
|
|
42237
|
+
if (!stack) continue;
|
|
42238
|
+
const slash = route.declaredAt.indexOf("/");
|
|
42239
|
+
if (slash < 0) continue;
|
|
42240
|
+
const logicalId = route.declaredAt.slice(slash + 1);
|
|
42241
|
+
const resource = stack.template.Resources?.[logicalId];
|
|
42242
|
+
if (!resource || resource.Type !== "AWS::ApiGatewayV2::Route") continue;
|
|
42243
|
+
const props = resource.Properties ?? {};
|
|
42244
|
+
const authType = props["AuthorizationType"];
|
|
42245
|
+
if (typeof authType !== "string" || authType === "" || authType.toUpperCase() === "NONE") continue;
|
|
42246
|
+
let authorizerName;
|
|
42247
|
+
if (authType === "AWS_IAM") authorizerName = "AWS_IAM";
|
|
42248
|
+
else {
|
|
42249
|
+
const ref = props["AuthorizerId"];
|
|
42250
|
+
authorizerName = (ref && typeof ref === "object" && "Ref" in ref && typeof ref.Ref === "string" ? ref.Ref : void 0) ?? `<authType=${authType}, AuthorizerId malformed>`;
|
|
42251
|
+
}
|
|
42252
|
+
out.push({
|
|
42253
|
+
declaredAt: route.declaredAt,
|
|
42254
|
+
authorizerName
|
|
42255
|
+
});
|
|
42256
|
+
}
|
|
42257
|
+
return out;
|
|
42258
|
+
}
|
|
42259
|
+
/**
|
|
41769
42260
|
* Detect the authorizer (if any) attached to a discovered route.
|
|
41770
42261
|
* Walks the original CFn resource for the route in `stack.template`.
|
|
41771
42262
|
*/
|
|
@@ -42895,6 +43386,191 @@ function amzDateOutsideSkew(amzDate, now) {
|
|
|
42895
43386
|
return Math.abs(ts.getTime() - now.getTime()) > 900 * 1e3;
|
|
42896
43387
|
}
|
|
42897
43388
|
|
|
43389
|
+
//#endregion
|
|
43390
|
+
//#region src/local/parameter-mapping.ts
|
|
43391
|
+
/**
|
|
43392
|
+
* Parameter-mapping resolver for HTTP API v2 service integrations
|
|
43393
|
+
* (`IntegrationSubtype` set, no Lambda).
|
|
43394
|
+
*
|
|
43395
|
+
* AWS API Gateway HTTP APIs let you wire a route directly to an AWS
|
|
43396
|
+
* SDK call via `RequestParameters` — a flat map whose KEY is the SDK
|
|
43397
|
+
* input parameter name (`QueueUrl`, `MessageBody`, `Source`, ...) and
|
|
43398
|
+
* whose VALUE is either a literal string or one of the dollar-prefixed
|
|
43399
|
+
* "selection expression" forms documented at
|
|
43400
|
+
* https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html
|
|
43401
|
+
*
|
|
43402
|
+
* Supported value forms (all bare and `${...}`-wrapped variants):
|
|
43403
|
+
* - `$request.header.<name>` — case-insensitive header lookup; multi-values comma-joined
|
|
43404
|
+
* - `$request.querystring.<key>` — case-SENSITIVE; multi-values comma-joined
|
|
43405
|
+
* - `$request.path.<param>` — path parameter from `{param}` route patterns
|
|
43406
|
+
* - `$request.path` — full request path (without stage)
|
|
43407
|
+
* - `$request.body` — entire request body as a string
|
|
43408
|
+
* - `$request.body.<jsonpath>` — JSONPath against the parsed JSON body
|
|
43409
|
+
* (recursive descent `..` and filter `?()` are NOT supported)
|
|
43410
|
+
* - `$context.<key>` — supported context variables
|
|
43411
|
+
* (requestId / accountId / domainName / identity.sourceIp / etc.)
|
|
43412
|
+
* - `$stageVariables.<key>` — values from the route's selected stage
|
|
43413
|
+
*
|
|
43414
|
+
* `${X} ${Y}` interpolation: any string containing `${...}` is treated
|
|
43415
|
+
* as a template literal — each placeholder is resolved independently
|
|
43416
|
+
* and the surrounding literal characters are preserved.
|
|
43417
|
+
*
|
|
43418
|
+
* Anything that is not a recognized selection expression AND does not
|
|
43419
|
+
* contain `${...}` is returned as a literal string.
|
|
43420
|
+
*
|
|
43421
|
+
* This module is **pure-functional and async-free** — every input is
|
|
43422
|
+
* derived from the HTTP request snapshot, route match, and the
|
|
43423
|
+
* pre-discovered route state. SDK invocation happens in the dispatcher
|
|
43424
|
+
* (see `httpv2-service-integration.ts`).
|
|
43425
|
+
*
|
|
43426
|
+
* Unresolved references (e.g. `$request.querystring.url` against a
|
|
43427
|
+
* request with no `url` parameter) resolve to the empty string —
|
|
43428
|
+
* matches deployed API Gateway behavior, which treats absent values
|
|
43429
|
+
* as `""` and passes them to the SDK call. The dispatcher's
|
|
43430
|
+
* per-subtype validator then surfaces the SDK's typed rejection.
|
|
43431
|
+
*/
|
|
43432
|
+
/**
|
|
43433
|
+
* Resolve a `RequestParameters` map (HTTP API v2 service integration
|
|
43434
|
+
* shape) against the incoming HTTP request. Keys are passed through
|
|
43435
|
+
* verbatim (they identify the SDK input parameter); only the VALUES
|
|
43436
|
+
* go through selection-expression resolution.
|
|
43437
|
+
*/
|
|
43438
|
+
function resolveServiceIntegrationParameters(parameters, ctx) {
|
|
43439
|
+
const resolved = {};
|
|
43440
|
+
for (const [key, rawValue] of Object.entries(parameters)) {
|
|
43441
|
+
if (typeof rawValue !== "string") return {
|
|
43442
|
+
kind: "error",
|
|
43443
|
+
reason: `RequestParameters[${JSON.stringify(key)}] must be a string (got ${typeof rawValue}: ${stringifyValue(rawValue)}).`
|
|
43444
|
+
};
|
|
43445
|
+
try {
|
|
43446
|
+
resolved[key] = resolveSelectionExpression(rawValue, ctx);
|
|
43447
|
+
} catch (err) {
|
|
43448
|
+
return {
|
|
43449
|
+
kind: "error",
|
|
43450
|
+
reason: `RequestParameters[${JSON.stringify(key)}]: ${err instanceof Error ? err.message : String(err)}`
|
|
43451
|
+
};
|
|
43452
|
+
}
|
|
43453
|
+
}
|
|
43454
|
+
return {
|
|
43455
|
+
kind: "ok",
|
|
43456
|
+
resolved
|
|
43457
|
+
};
|
|
43458
|
+
}
|
|
43459
|
+
/**
|
|
43460
|
+
* Resolve a single selection-expression string. Public for unit
|
|
43461
|
+
* testing — production callers use {@link resolveServiceIntegrationParameters}.
|
|
43462
|
+
*
|
|
43463
|
+
* Three shapes:
|
|
43464
|
+
* 1. Pure bare reference (`$request.querystring.url`) — resolved
|
|
43465
|
+
* and returned as-is. Whole-string match.
|
|
43466
|
+
* 2. Embedded `${...}` interpolation — every placeholder is resolved
|
|
43467
|
+
* and concatenated with the surrounding literals.
|
|
43468
|
+
* 3. Anything else — returned verbatim as a literal.
|
|
43469
|
+
*/
|
|
43470
|
+
function resolveSelectionExpression(input, ctx) {
|
|
43471
|
+
if (input.startsWith("$") && !input.includes("${")) {
|
|
43472
|
+
const resolved = resolveSingleReference(input, ctx);
|
|
43473
|
+
if (resolved !== void 0) return resolved;
|
|
43474
|
+
return input;
|
|
43475
|
+
}
|
|
43476
|
+
if (input.includes("${")) return interpolate(input, ctx);
|
|
43477
|
+
return input;
|
|
43478
|
+
}
|
|
43479
|
+
/**
|
|
43480
|
+
* Walk a `${...}`-templated string and emit the concatenated result.
|
|
43481
|
+
* Per AWS docs, `${X}` may contain any of the selection-expression
|
|
43482
|
+
* forms recognized in bare form.
|
|
43483
|
+
*/
|
|
43484
|
+
function interpolate(input, ctx) {
|
|
43485
|
+
let out = "";
|
|
43486
|
+
let i = 0;
|
|
43487
|
+
while (i < input.length) {
|
|
43488
|
+
const next = input.indexOf("${", i);
|
|
43489
|
+
if (next === -1) {
|
|
43490
|
+
out += input.slice(i);
|
|
43491
|
+
break;
|
|
43492
|
+
}
|
|
43493
|
+
out += input.slice(i, next);
|
|
43494
|
+
const end = input.indexOf("}", next + 2);
|
|
43495
|
+
if (end === -1) throw new Error(`unclosed '\${...}' interpolation in selection expression`);
|
|
43496
|
+
const resolved = resolveSingleReference("$" + input.slice(next + 2, end), ctx);
|
|
43497
|
+
out += resolved ?? "";
|
|
43498
|
+
i = end + 1;
|
|
43499
|
+
}
|
|
43500
|
+
return out;
|
|
43501
|
+
}
|
|
43502
|
+
/**
|
|
43503
|
+
* Resolve one selection-expression reference (without `${...}`
|
|
43504
|
+
* wrapping). Returns `undefined` when the reference's PREFIX is not
|
|
43505
|
+
* a recognized form (so the bare-form caller can fall through to
|
|
43506
|
+
* literal-pass-through); returns `""` when the prefix matched but
|
|
43507
|
+
* the referenced datum was absent (AWS-deployed behavior).
|
|
43508
|
+
*/
|
|
43509
|
+
function resolveSingleReference(ref, ctx) {
|
|
43510
|
+
if (ref === "$request.body") return ctx.body;
|
|
43511
|
+
if (ref === "$request.path") return ctx.requestPath;
|
|
43512
|
+
if (ref.startsWith("$request.body.")) {
|
|
43513
|
+
const path = ref.substring(14);
|
|
43514
|
+
return resolveBodyJsonPath(ctx.body, path);
|
|
43515
|
+
}
|
|
43516
|
+
if (ref.startsWith("$request.header.")) {
|
|
43517
|
+
const name = ref.substring(16).toLowerCase();
|
|
43518
|
+
return ctx.headers[name] ?? "";
|
|
43519
|
+
}
|
|
43520
|
+
if (ref.startsWith("$request.querystring.")) {
|
|
43521
|
+
const key = ref.substring(21);
|
|
43522
|
+
return ctx.queryString[key] ?? "";
|
|
43523
|
+
}
|
|
43524
|
+
if (ref.startsWith("$request.path.")) {
|
|
43525
|
+
const key = ref.substring(14);
|
|
43526
|
+
return ctx.pathParameters[key] ?? "";
|
|
43527
|
+
}
|
|
43528
|
+
if (ref.startsWith("$context.")) {
|
|
43529
|
+
const key = ref.substring(9);
|
|
43530
|
+
return ctx.context[key] ?? "";
|
|
43531
|
+
}
|
|
43532
|
+
if (ref.startsWith("$stageVariables.")) {
|
|
43533
|
+
const key = ref.substring(16);
|
|
43534
|
+
return ctx.stageVariables[key] ?? "";
|
|
43535
|
+
}
|
|
43536
|
+
}
|
|
43537
|
+
/**
|
|
43538
|
+
* Resolve a simple JSON path against the request body. Per AWS docs:
|
|
43539
|
+
*
|
|
43540
|
+
* - The body is JSON-parsed (best-effort; non-JSON → empty string).
|
|
43541
|
+
* - Path segments are `.`-separated. Array indexing via `[N]` is
|
|
43542
|
+
* supported.
|
|
43543
|
+
* - Recursive descent `..` and filter expressions `?()` are NOT
|
|
43544
|
+
* supported and produce `""` (matches AWS rejection at deploy
|
|
43545
|
+
* time, but we degrade gracefully at runtime).
|
|
43546
|
+
*
|
|
43547
|
+
* Non-string leaves are stringified with `JSON.stringify` so they can
|
|
43548
|
+
* round-trip into the SDK call's string-typed input fields.
|
|
43549
|
+
*/
|
|
43550
|
+
function resolveBodyJsonPath(body, path) {
|
|
43551
|
+
if (path.startsWith(".") || path.includes("..") || path.includes("?(")) return "";
|
|
43552
|
+
let parsed;
|
|
43553
|
+
try {
|
|
43554
|
+
parsed = JSON.parse(body);
|
|
43555
|
+
} catch {
|
|
43556
|
+
return "";
|
|
43557
|
+
}
|
|
43558
|
+
const segments = path.split(/\.|\[(\d+)\]/).filter((s) => s !== void 0 && s !== "");
|
|
43559
|
+
let cursor = parsed;
|
|
43560
|
+
for (const seg of segments) {
|
|
43561
|
+
if (cursor === null || cursor === void 0) return "";
|
|
43562
|
+
if (typeof cursor !== "object") return "";
|
|
43563
|
+
if (Array.isArray(cursor)) {
|
|
43564
|
+
const idx = Number(seg);
|
|
43565
|
+
if (!Number.isInteger(idx)) return "";
|
|
43566
|
+
cursor = cursor[idx];
|
|
43567
|
+
} else cursor = cursor[seg];
|
|
43568
|
+
}
|
|
43569
|
+
if (cursor === void 0 || cursor === null) return "";
|
|
43570
|
+
if (typeof cursor === "string") return cursor;
|
|
43571
|
+
return JSON.stringify(cursor);
|
|
43572
|
+
}
|
|
43573
|
+
|
|
42898
43574
|
//#endregion
|
|
42899
43575
|
//#region src/local/http-server.ts
|
|
42900
43576
|
/**
|
|
@@ -42995,6 +43671,10 @@ async function handleRequest(req, res, state, opts) {
|
|
|
42995
43671
|
writeNotImplemented(res, match.route.unsupported.reason);
|
|
42996
43672
|
return;
|
|
42997
43673
|
}
|
|
43674
|
+
if (match.route.serviceIntegration) {
|
|
43675
|
+
await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts);
|
|
43676
|
+
return;
|
|
43677
|
+
}
|
|
42998
43678
|
const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
|
|
42999
43679
|
const clientCert = opts.mtls ? extractClientCert(req) : void 0;
|
|
43000
43680
|
const snapshot = {
|
|
@@ -43372,9 +44052,12 @@ function lowercaseSingularHeaders(raw) {
|
|
|
43372
44052
|
return out;
|
|
43373
44053
|
}
|
|
43374
44054
|
/**
|
|
43375
|
-
* Parse query string into a singular
|
|
43376
|
-
*
|
|
43377
|
-
*
|
|
44055
|
+
* Parse query string into a singular map. Multi-value keys are
|
|
44056
|
+
* comma-joined in order of appearance (`?foo=a&foo=b` -> `foo: 'a,b'`)
|
|
44057
|
+
* — matches the contract documented at
|
|
44058
|
+
* `src/local/parameter-mapping.ts:14` ("multi-values comma-joined")
|
|
44059
|
+
* AND deployed API Gateway behavior. Used by both the authorizer pass
|
|
44060
|
+
* and the HTTP API v2 service-integration parameter mapper.
|
|
43378
44061
|
*/
|
|
43379
44062
|
function parseQueryStringSingular(rawUrl) {
|
|
43380
44063
|
const q = rawUrl.indexOf("?");
|
|
@@ -43395,7 +44078,8 @@ function parseQueryStringSingular(rawUrl) {
|
|
|
43395
44078
|
try {
|
|
43396
44079
|
value = decodeURIComponent(rawValue);
|
|
43397
44080
|
} catch {}
|
|
43398
|
-
out[key]
|
|
44081
|
+
const prev = out[key];
|
|
44082
|
+
out[key] = prev === void 0 ? value : `${prev},${value}`;
|
|
43399
44083
|
}
|
|
43400
44084
|
return out;
|
|
43401
44085
|
}
|
|
@@ -43441,6 +44125,120 @@ function writeError(res, statusCode, body = "{\"message\":\"Internal server erro
|
|
|
43441
44125
|
res.end(body);
|
|
43442
44126
|
}
|
|
43443
44127
|
/**
|
|
44128
|
+
* Handle an HTTP API v2 service-integration route (#458). The route
|
|
44129
|
+
* carries `serviceIntegration: { subtype, requestParameters, responseParameters? }`
|
|
44130
|
+
* — no Lambda backs it. Flow:
|
|
44131
|
+
*
|
|
44132
|
+
* 1. Build a {@link RequestParameterContext} from the HTTP request +
|
|
44133
|
+
* route match (path parameters, query string, headers, context
|
|
44134
|
+
* variables, stage variables).
|
|
44135
|
+
* 2. Resolve every `RequestParameters` value against that context via
|
|
44136
|
+
* `parameter-mapping.ts`. Per-parameter unresolved refs degrade to
|
|
44137
|
+
* `""` (matches AWS deployed behavior).
|
|
44138
|
+
* 3. Dispatch to the per-subtype SDK adapter in
|
|
44139
|
+
* `httpv2-service-integration.ts`. Per-request `Region` parameter
|
|
44140
|
+
* overrides the server's `opts.defaultRegion`; absent both surfaces
|
|
44141
|
+
* a 400.
|
|
44142
|
+
* 4. Apply per-status-code `ResponseParameters` overlay (header /
|
|
44143
|
+
* statuscode overwrites).
|
|
44144
|
+
* 5. Write the result to the HTTP client.
|
|
44145
|
+
*
|
|
44146
|
+
* Authorizer pass: NOT yet wired (current scope is dispatch only). When
|
|
44147
|
+
* a service-integration route carries an authorizer, the discovery layer
|
|
44148
|
+
* leaves it in place but the server short-circuits to dispatch BEFORE the
|
|
44149
|
+
* auth pass. A follow-up PR can hoist the auth pass earlier — keeping it
|
|
44150
|
+
* out of this PR limits the blast radius.
|
|
44151
|
+
*/
|
|
44152
|
+
async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
|
|
44153
|
+
const route = match.route;
|
|
44154
|
+
const svc = route.serviceIntegration;
|
|
44155
|
+
if (!svc) {
|
|
44156
|
+
writeError(res, 500);
|
|
44157
|
+
return;
|
|
44158
|
+
}
|
|
44159
|
+
const rawUrl = req.url ?? "/";
|
|
44160
|
+
const headersFlat = flattenHeadersForMapping(req);
|
|
44161
|
+
const queryString = parseQueryStringSingular(rawUrl);
|
|
44162
|
+
const requestPath = rawUrl.split("?")[0] ?? "/";
|
|
44163
|
+
const context = buildServiceIntegrationContextVars(req, route);
|
|
44164
|
+
const ctx = {
|
|
44165
|
+
headers: headersFlat,
|
|
44166
|
+
queryString,
|
|
44167
|
+
pathParameters: match.pathParameters,
|
|
44168
|
+
requestPath,
|
|
44169
|
+
body: bodyBuf.toString("utf8"),
|
|
44170
|
+
context,
|
|
44171
|
+
stageVariables: route.stageVariables ?? {}
|
|
44172
|
+
};
|
|
44173
|
+
const outcome = resolveServiceIntegrationParameters(svc.requestParameters, ctx);
|
|
44174
|
+
if (outcome.kind === "error") {
|
|
44175
|
+
getLogger().warn(`[${route.declaredAt}] ${outcome.reason}`);
|
|
44176
|
+
const body = JSON.stringify({
|
|
44177
|
+
message: "Invalid integration mapping",
|
|
44178
|
+
reason: outcome.reason
|
|
44179
|
+
});
|
|
44180
|
+
res.statusCode = 500;
|
|
44181
|
+
res.setHeader("content-type", "application/json");
|
|
44182
|
+
res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
|
|
44183
|
+
res.end(body);
|
|
44184
|
+
return;
|
|
44185
|
+
}
|
|
44186
|
+
const result = await dispatchServiceIntegration(svc.subtype, outcome.resolved, opts.defaultRegion ?? "");
|
|
44187
|
+
const responseCtx = {
|
|
44188
|
+
context,
|
|
44189
|
+
stageVariables: route.stageVariables ?? {}
|
|
44190
|
+
};
|
|
44191
|
+
const finalResult = applyResponseParameters(result, svc.responseParameters, responseCtx);
|
|
44192
|
+
res.statusCode = finalResult.statusCode;
|
|
44193
|
+
for (const [name, value] of Object.entries(finalResult.headers)) res.setHeader(name, value);
|
|
44194
|
+
res.setHeader("content-length", String(Buffer.byteLength(finalResult.body, "utf-8")));
|
|
44195
|
+
res.end(finalResult.body);
|
|
44196
|
+
}
|
|
44197
|
+
/**
|
|
44198
|
+
* Flatten Node's `req.headers` to the single-string-per-key shape the
|
|
44199
|
+
* parameter-mapping resolver expects (`$request.header.<name>` is
|
|
44200
|
+
* documented as comma-joined multi-value). Header NAMES are lowercased
|
|
44201
|
+
* because AWS docs document `$request.header.<name>` as case-insensitive.
|
|
44202
|
+
*/
|
|
44203
|
+
function flattenHeadersForMapping(req) {
|
|
44204
|
+
const out = {};
|
|
44205
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
44206
|
+
const lower = name.toLowerCase();
|
|
44207
|
+
if (Array.isArray(value)) out[lower] = value.join(", ");
|
|
44208
|
+
else if (typeof value === "string") out[lower] = value;
|
|
44209
|
+
}
|
|
44210
|
+
return out;
|
|
44211
|
+
}
|
|
44212
|
+
/**
|
|
44213
|
+
* Build the subset of AWS context variables the service-integration
|
|
44214
|
+
* parameter-mapping resolver needs to surface (per AWS docs
|
|
44215
|
+
* https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html).
|
|
44216
|
+
*
|
|
44217
|
+
* We populate the most-commonly-referenced fields with realistic values
|
|
44218
|
+
* (`requestId` is fresh per call, `accountId` / `domainName` are mock
|
|
44219
|
+
* but stable). Selection expressions against context variables we
|
|
44220
|
+
* don't model resolve to `""` — matches the AWS-deployed behavior of
|
|
44221
|
+
* absent values.
|
|
44222
|
+
*/
|
|
44223
|
+
function buildServiceIntegrationContextVars(req, route) {
|
|
44224
|
+
const requestId = `cdkd-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
44225
|
+
const sourceIp = req.socket.remoteAddress ?? "127.0.0.1";
|
|
44226
|
+
return {
|
|
44227
|
+
requestId,
|
|
44228
|
+
accountId: "123456789012",
|
|
44229
|
+
apiId: route.apiLogicalId ?? "local",
|
|
44230
|
+
stage: route.stage,
|
|
44231
|
+
"identity.sourceIp": sourceIp,
|
|
44232
|
+
"identity.userAgent": Array.isArray(req.headers["user-agent"]) || typeof req.headers["user-agent"] === "string" ? String(req.headers["user-agent"]) : "",
|
|
44233
|
+
domainName: "localhost",
|
|
44234
|
+
httpMethod: req.method ?? "GET",
|
|
44235
|
+
path: (req.url ?? "/").split("?")[0] ?? "/",
|
|
44236
|
+
protocol: "HTTP/1.1",
|
|
44237
|
+
requestTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
44238
|
+
requestTimeEpoch: String(Date.now())
|
|
44239
|
+
};
|
|
44240
|
+
}
|
|
44241
|
+
/**
|
|
43444
44242
|
* Write the 501 Not Implemented response surfaced for routes the
|
|
43445
44243
|
* discovery layer flagged as `unsupported`. The integration's reason
|
|
43446
44244
|
* (e.g. "MOCK integration is not emulated", "WebSocket APIs are not
|
|
@@ -44142,12 +44940,13 @@ async function localStartApiCommand(target, options) {
|
|
|
44142
44940
|
inlineTmpDirs,
|
|
44143
44941
|
layerTmpDirs,
|
|
44144
44942
|
stateByStack,
|
|
44943
|
+
skipPull: options.pull === false,
|
|
44145
44944
|
...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn }
|
|
44146
44945
|
});
|
|
44147
44946
|
specs.set(logicalId, spec);
|
|
44148
44947
|
}
|
|
44149
44948
|
const distinctImages = /* @__PURE__ */ new Set();
|
|
44150
|
-
for (const spec of specs.values()) distinctImages.add(resolveRuntimeImage(spec.lambda.runtime));
|
|
44949
|
+
for (const spec of specs.values()) if (spec.kind === "zip") distinctImages.add(resolveRuntimeImage(spec.lambda.runtime));
|
|
44151
44950
|
for (const image of distinctImages) await pullImage(image, options.pull === false);
|
|
44152
44951
|
return {
|
|
44153
44952
|
routes: routesWithAuth,
|
|
@@ -44176,13 +44975,23 @@ async function localStartApiCommand(target, options) {
|
|
|
44176
44975
|
/**
|
|
44177
44976
|
* Compute the watched-asset list from a spec map. Pure helper —
|
|
44178
44977
|
* keeps the side-effect (`lastAssetPaths.value = ...`) confined to
|
|
44179
|
-
* the post-swap call sites (initial boot + post-reload).
|
|
44180
|
-
* is either the unzipped asset directory or the
|
|
44181
|
-
* both are watch-worthy.
|
|
44978
|
+
* the post-swap call sites (initial boot + post-reload). For ZIP
|
|
44979
|
+
* Lambdas `codeDir` is either the unzipped asset directory or the
|
|
44980
|
+
* inline-code tmpdir; both are watch-worthy. IMAGE Lambdas
|
|
44981
|
+
* (`kind: 'image'`) don't have a host-side bind-mount source — the
|
|
44982
|
+
* code is baked into the docker image at build time. Their build
|
|
44983
|
+
* context (Dockerfile + source directory) is rebuilt on every
|
|
44984
|
+
* reload via `synthesizeAndBuild` → `buildContainerSpec` →
|
|
44985
|
+
* `resolveContainerImageForStartApi`, so a source edit DOES trigger
|
|
44986
|
+
* rebuild AND the deterministic `image` tag changes — but watching
|
|
44987
|
+
* the build-context dir explicitly here is deferred to a follow-up
|
|
44988
|
+
* (the watched-asset list is currently sourced from `cdk.out/`
|
|
44989
|
+
* which transitively covers most container-Lambda asset dirs since
|
|
44990
|
+
* `cdk synth` re-stages them on every synth call).
|
|
44182
44991
|
*/
|
|
44183
44992
|
const computeAssetPaths = (specs) => {
|
|
44184
44993
|
const assetPaths = /* @__PURE__ */ new Set();
|
|
44185
|
-
for (const spec of specs.values()) assetPaths.add(spec.codeDir);
|
|
44994
|
+
for (const spec of specs.values()) if (spec.kind === "zip") assetPaths.add(spec.codeDir);
|
|
44186
44995
|
return [...assetPaths];
|
|
44187
44996
|
};
|
|
44188
44997
|
const initialMaterial = await synthesizeAndBuild();
|
|
@@ -44191,6 +45000,7 @@ async function localStartApiCommand(target, options) {
|
|
|
44191
45000
|
warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
|
|
44192
45001
|
sigV4CredentialsLoader = defaultCredentialsLoader();
|
|
44193
45002
|
warnIamRoutes(initialMaterial.routes);
|
|
45003
|
+
warnIgnoredServiceIntegrationAuthorizers(initialMaterial.routes, initialMaterial.stacks ?? []);
|
|
44194
45004
|
let maxTimeoutSec = 0;
|
|
44195
45005
|
for (const spec of initialMaterial.specs.values()) if (spec.lambda.timeoutSec > maxTimeoutSec) maxTimeoutSec = spec.lambda.timeoutSec;
|
|
44196
45006
|
const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
|
|
@@ -44215,6 +45025,7 @@ async function localStartApiCommand(target, options) {
|
|
|
44215
45025
|
for (const result of handles) if (result.status === "fulfilled") groupPool.release(result.value);
|
|
44216
45026
|
else logger.warn(`Pre-warm failed for one Lambda in ${group.displayName} (cold start cost will apply on first request): ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
44217
45027
|
}
|
|
45028
|
+
const defaultRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? void 0;
|
|
44218
45029
|
const started = await startApiServer({
|
|
44219
45030
|
state: groupState,
|
|
44220
45031
|
rieTimeoutMs,
|
|
@@ -44226,7 +45037,8 @@ async function localStartApiCommand(target, options) {
|
|
|
44226
45037
|
jwksWarnedUrls,
|
|
44227
45038
|
sigV4CredentialsLoader,
|
|
44228
45039
|
sigV4WarnedForeignIds,
|
|
44229
|
-
sigV4AllowUnverified: options.allowUnverifiedSigv4 === true
|
|
45040
|
+
sigV4AllowUnverified: options.allowUnverifiedSigv4 === true,
|
|
45041
|
+
...defaultRegion && { defaultRegion }
|
|
44230
45042
|
});
|
|
44231
45043
|
servers.push({
|
|
44232
45044
|
group,
|
|
@@ -44355,7 +45167,7 @@ function uniqueLambdaIds(routes, routesWithAuth) {
|
|
|
44355
45167
|
const seen = /* @__PURE__ */ new Set();
|
|
44356
45168
|
const out = [];
|
|
44357
45169
|
for (const r of routes) {
|
|
44358
|
-
if (r.unsupported || r.mockCors) continue;
|
|
45170
|
+
if (r.unsupported || r.mockCors || r.serviceIntegration) continue;
|
|
44359
45171
|
if (r.lambdaLogicalId.length === 0) continue;
|
|
44360
45172
|
if (!seen.has(r.lambdaLogicalId)) {
|
|
44361
45173
|
seen.add(r.lambdaLogicalId);
|
|
@@ -44363,7 +45175,7 @@ function uniqueLambdaIds(routes, routesWithAuth) {
|
|
|
44363
45175
|
}
|
|
44364
45176
|
}
|
|
44365
45177
|
for (const entry of routesWithAuth) {
|
|
44366
|
-
if (entry.route.unsupported || entry.route.mockCors) continue;
|
|
45178
|
+
if (entry.route.unsupported || entry.route.mockCors || entry.route.serviceIntegration) continue;
|
|
44367
45179
|
const auth = entry.authorizer;
|
|
44368
45180
|
if (!auth) continue;
|
|
44369
45181
|
if (auth.kind === "lambda-token" || auth.kind === "lambda-request") {
|
|
@@ -44448,6 +45260,27 @@ function warnIamRoutes(routesWithAuth) {
|
|
|
44448
45260
|
return true;
|
|
44449
45261
|
}
|
|
44450
45262
|
/**
|
|
45263
|
+
* #458 / PR #500 review: emit a one-line warn naming every service-
|
|
45264
|
+
* integration route whose source CFn resource declares an authorizer
|
|
45265
|
+
* (HTTP API v2 routes with `AuthorizationType !== 'NONE'`). The
|
|
45266
|
+
* dispatcher in `http-server.ts` runs the SDK call BEFORE the
|
|
45267
|
+
* authorizer pass would fire, so without this warn a CDK app that
|
|
45268
|
+
* wires JWT / Lambda / Cognito / IAM authorizers onto service
|
|
45269
|
+
* integrations would silently let every local request reach the SDK
|
|
45270
|
+
* call without auth. Threading the auth pass through the
|
|
45271
|
+
* service-integration dispatcher is a follow-up issue. Returns the
|
|
45272
|
+
* number of warned routes so tests can assert the firing path; the
|
|
45273
|
+
* value is otherwise unused.
|
|
45274
|
+
*/
|
|
45275
|
+
function warnIgnoredServiceIntegrationAuthorizers(routesWithAuth, stacks) {
|
|
45276
|
+
const logger = getLogger();
|
|
45277
|
+
const ignored = findIgnoredServiceIntegrationAuthorizers(stacks, routesWithAuth.map((entry) => entry.route));
|
|
45278
|
+
if (ignored.length === 0) return 0;
|
|
45279
|
+
logger.warn(`${ignored.length} HTTP API v2 service-integration route(s) declare an authorizer but cdkd local start-api dispatches the SDK call BEFORE the authorizer pass — every local request reaches the SDK call WITHOUT authentication. This is a deferred feature; see https://github.com/go-to-k/cdkd/issues/502 for the follow-up tracking issue.`);
|
|
45280
|
+
for (const entry of ignored) logger.warn(` - ${entry.declaredAt}: authorizer '${entry.authorizerName}' is configured but ignored`);
|
|
45281
|
+
return ignored.length;
|
|
45282
|
+
}
|
|
45283
|
+
/**
|
|
44451
45284
|
* Build the per-Lambda container spec — code dir, env vars (template +
|
|
44452
45285
|
* --env-vars overlay), STS-issued creds when --assume-role names this
|
|
44453
45286
|
* Lambda, optional --debug-port reservation. Errors out with a clear
|
|
@@ -44455,10 +45288,19 @@ function warnIamRoutes(routesWithAuth) {
|
|
|
44455
45288
|
* missing, runtime not supported).
|
|
44456
45289
|
*/
|
|
44457
45290
|
async function buildContainerSpec(args) {
|
|
44458
|
-
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, layerRoleArn } = args;
|
|
45291
|
+
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn } = args;
|
|
44459
45292
|
const lambda = resolveLambdaByLogicalId(logicalId, stacks);
|
|
44460
|
-
|
|
44461
|
-
|
|
45293
|
+
let codeDir;
|
|
45294
|
+
let optDir;
|
|
45295
|
+
let imageRef;
|
|
45296
|
+
let platform;
|
|
45297
|
+
if (lambda.kind === "zip") {
|
|
45298
|
+
codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
45299
|
+
optDir = await materializeLambdaLayers$1(lambda.layers, layerTmpDirs, layerRoleArn);
|
|
45300
|
+
} else {
|
|
45301
|
+
imageRef = (await resolveContainerImageForStartApi(lambda, skipPull)).imageRef;
|
|
45302
|
+
platform = architectureToPlatform(lambda.architecture);
|
|
45303
|
+
}
|
|
44462
45304
|
let templateEnv = getTemplateEnv$1(lambda.resource);
|
|
44463
45305
|
const stateBundle = stateByStack.get(lambda.stack.stackName);
|
|
44464
45306
|
let stateAudit;
|
|
@@ -44498,7 +45340,8 @@ async function buildContainerSpec(args) {
|
|
|
44498
45340
|
target: "/tmp",
|
|
44499
45341
|
sizeMb: lambda.ephemeralStorageMb
|
|
44500
45342
|
} : void 0;
|
|
44501
|
-
return {
|
|
45343
|
+
if (lambda.kind === "zip") return {
|
|
45344
|
+
kind: "zip",
|
|
44502
45345
|
lambda,
|
|
44503
45346
|
codeDir,
|
|
44504
45347
|
env: dockerEnv,
|
|
@@ -44507,6 +45350,60 @@ async function buildContainerSpec(args) {
|
|
|
44507
45350
|
...debugPort !== void 0 && { debugPort },
|
|
44508
45351
|
...tmpfs !== void 0 && { tmpfs }
|
|
44509
45352
|
};
|
|
45353
|
+
return {
|
|
45354
|
+
kind: "image",
|
|
45355
|
+
lambda,
|
|
45356
|
+
image: imageRef,
|
|
45357
|
+
platform,
|
|
45358
|
+
command: lambda.imageConfig.command ?? [],
|
|
45359
|
+
...lambda.imageConfig.entryPoint !== void 0 && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
|
|
45360
|
+
...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory },
|
|
45361
|
+
env: dockerEnv,
|
|
45362
|
+
containerHost,
|
|
45363
|
+
...debugPort !== void 0 && { debugPort },
|
|
45364
|
+
...tmpfs !== void 0 && { tmpfs }
|
|
45365
|
+
};
|
|
45366
|
+
}
|
|
45367
|
+
/**
|
|
45368
|
+
* Resolve a container Lambda's local docker image — local build from
|
|
45369
|
+
* `cdk.out` asset manifest first, ECR-pull fallback when the asset
|
|
45370
|
+
* manifest has no matching entry. Mirrors `cdkd local invoke`'s
|
|
45371
|
+
* `resolveContainerImagePlan` shape; the start-api server doesn't
|
|
45372
|
+
* need the no-build flag (deterministic-tag cache reuse is automatic
|
|
45373
|
+
* across reloads because the per-Lambda tag is content-addressed).
|
|
45374
|
+
*
|
|
45375
|
+
* Same-account / same-region only on the ECR-pull path (matches the
|
|
45376
|
+
* `cdkd local invoke` PR 5 of #224 boundary). Cross-account /
|
|
45377
|
+
* cross-region ECR pull is the W2-1 deferred follow-up.
|
|
45378
|
+
*/
|
|
45379
|
+
async function resolveContainerImageForStartApi(lambda, skipPull) {
|
|
45380
|
+
const logger = getLogger();
|
|
45381
|
+
const localBuild = await resolveLocalBuildPlan$1(lambda);
|
|
45382
|
+
if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, { architecture: lambda.architecture }) };
|
|
45383
|
+
if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI cdkd can authenticate against. Re-synthesize the CDK app (so cdk.out includes the build context) or deploy the image to ECR first.`);
|
|
45384
|
+
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
|
|
45385
|
+
return { imageRef: await pullEcrImage(lambda.imageUri, { skipPull }) };
|
|
45386
|
+
}
|
|
45387
|
+
/**
|
|
45388
|
+
* Look up the docker image asset that backs a container Lambda.
|
|
45389
|
+
* Returns `undefined` when the asset manifest has no matching entry —
|
|
45390
|
+
* the caller falls back to the ECR-pull path.
|
|
45391
|
+
*
|
|
45392
|
+
* Mirrors `local-invoke.ts:resolveLocalBuildPlan`; kept separate so
|
|
45393
|
+
* the two commands evolve their asset-lookup heuristics independently.
|
|
45394
|
+
*/
|
|
45395
|
+
async function resolveLocalBuildPlan$1(lambda) {
|
|
45396
|
+
const manifestPath = lambda.stack.assetManifestPath;
|
|
45397
|
+
if (!manifestPath) return void 0;
|
|
45398
|
+
const cdkOutDir = path.dirname(manifestPath);
|
|
45399
|
+
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
|
|
45400
|
+
if (!manifest) return void 0;
|
|
45401
|
+
const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
|
|
45402
|
+
if (!entry) return void 0;
|
|
45403
|
+
return {
|
|
45404
|
+
asset: entry.asset,
|
|
45405
|
+
cdkOutDir
|
|
45406
|
+
};
|
|
44510
45407
|
}
|
|
44511
45408
|
/**
|
|
44512
45409
|
* Build the `/opt` bind-mount source for a Lambda's layers. Mirrors
|
|
@@ -44568,15 +45465,23 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
|
|
|
44568
45465
|
const resource = stack.template.Resources?.[logicalId];
|
|
44569
45466
|
if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
|
|
44570
45467
|
const props = resource.Properties ?? {};
|
|
44571
|
-
const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
|
|
44572
|
-
const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
|
|
44573
45468
|
const memoryMb = typeof props["MemorySize"] === "number" ? props["MemorySize"] : 128;
|
|
44574
45469
|
const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
|
|
44575
|
-
if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property. Container-image Lambdas (Code.ImageUri) are not supported in cdkd local start-api v1.`);
|
|
44576
|
-
if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
|
|
44577
45470
|
const code = props["Code"] ?? {};
|
|
44578
|
-
const imageUri = code["ImageUri"];
|
|
44579
|
-
if (
|
|
45471
|
+
const imageUri = extractImageUri(code["ImageUri"]);
|
|
45472
|
+
if (imageUri !== void 0) return resolveImageLambda({
|
|
45473
|
+
stack,
|
|
45474
|
+
logicalId,
|
|
45475
|
+
resource,
|
|
45476
|
+
props,
|
|
45477
|
+
memoryMb,
|
|
45478
|
+
timeoutSec,
|
|
45479
|
+
imageUri
|
|
45480
|
+
});
|
|
45481
|
+
const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
|
|
45482
|
+
const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
|
|
45483
|
+
if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. cdkd local start-api cannot tell if this is a ZIP or a container Lambda.`);
|
|
45484
|
+
if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
|
|
44580
45485
|
const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
|
|
44581
45486
|
let codePath = null;
|
|
44582
45487
|
if (!inlineCode) codePath = resolveAssetCodePath(stack, logicalId, resource);
|
|
@@ -44600,6 +45505,66 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
|
|
|
44600
45505
|
throw new Error(`No AWS::Lambda::Function resource named '${logicalId}' found in target stacks. This is likely a synthesis bug — the route-discovery phase resolved a route to this logical ID.`);
|
|
44601
45506
|
}
|
|
44602
45507
|
/**
|
|
45508
|
+
* Extract `Code.ImageUri` across the shapes CDK actually synthesizes.
|
|
45509
|
+
* Mirrors the simpler subset of `lambda-resolver.ts:extractImageUri`
|
|
45510
|
+
* scoped to the shapes `cdkd local start-api` consumes — flat string
|
|
45511
|
+
* and `Fn::Sub` (the canonical asset shape for
|
|
45512
|
+
* `lambda.DockerImageCode.fromImageAsset`). `Fn::Join` shapes for
|
|
45513
|
+
* `lambda.DockerImageCode.fromEcr` are deferred to a follow-up: the
|
|
45514
|
+
* start-api boot flow doesn't yet load cdkd state up front, and the
|
|
45515
|
+
* `Fn::Join` resolver needs it to recover same-stack ECR repository
|
|
45516
|
+
* URIs. When the user hits the unsupported shape, the downstream
|
|
45517
|
+
* resolveLocalBuildPlan / pullEcrImage path surfaces a clear error.
|
|
45518
|
+
*
|
|
45519
|
+
* Returns `undefined` when the field is absent or non-recognized,
|
|
45520
|
+
* which routes the caller to the ZIP branch (with its existing
|
|
45521
|
+
* "no Runtime / no Handler" validations).
|
|
45522
|
+
*/
|
|
45523
|
+
function extractImageUri(value) {
|
|
45524
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
45525
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
45526
|
+
const sub = value["Fn::Sub"];
|
|
45527
|
+
if (typeof sub === "string" && sub.length > 0) return sub;
|
|
45528
|
+
if (Array.isArray(sub) && typeof sub[0] === "string") return sub[0];
|
|
45529
|
+
}
|
|
45530
|
+
}
|
|
45531
|
+
/**
|
|
45532
|
+
* Build the IMAGE-variant `ResolvedStartApiLambda` from a Lambda
|
|
45533
|
+
* template entry with `Code.ImageUri`. Mirrors
|
|
45534
|
+
* `lambda-resolver.ts:extractImageLambdaProperties` but trimmed to the
|
|
45535
|
+
* fields `cdkd local start-api` actually consumes.
|
|
45536
|
+
*/
|
|
45537
|
+
function resolveImageLambda(args) {
|
|
45538
|
+
const { stack, logicalId, resource, props, memoryMb, timeoutSec, imageUri } = args;
|
|
45539
|
+
const rawImageConfig = props["ImageConfig"] ?? {};
|
|
45540
|
+
const imageConfig = {};
|
|
45541
|
+
if (Array.isArray(rawImageConfig["Command"])) imageConfig.command = rawImageConfig["Command"].filter((s) => typeof s === "string");
|
|
45542
|
+
if (Array.isArray(rawImageConfig["EntryPoint"])) imageConfig.entryPoint = rawImageConfig["EntryPoint"].filter((s) => typeof s === "string");
|
|
45543
|
+
if (typeof rawImageConfig["WorkingDirectory"] === "string") imageConfig.workingDirectory = rawImageConfig["WorkingDirectory"];
|
|
45544
|
+
const arches = props["Architectures"];
|
|
45545
|
+
let architecture = "x86_64";
|
|
45546
|
+
if (Array.isArray(arches) && arches.length > 0) {
|
|
45547
|
+
const first = arches[0];
|
|
45548
|
+
if (first === "arm64") architecture = "arm64";
|
|
45549
|
+
else if (first === "x86_64") architecture = "x86_64";
|
|
45550
|
+
else throw new Error(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'. cdkd local start-api supports x86_64 and arm64.`);
|
|
45551
|
+
}
|
|
45552
|
+
const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
|
|
45553
|
+
return {
|
|
45554
|
+
kind: "image",
|
|
45555
|
+
stack,
|
|
45556
|
+
logicalId,
|
|
45557
|
+
resource,
|
|
45558
|
+
memoryMb,
|
|
45559
|
+
timeoutSec,
|
|
45560
|
+
imageUri,
|
|
45561
|
+
imageConfig,
|
|
45562
|
+
architecture,
|
|
45563
|
+
layers: [],
|
|
45564
|
+
...ephemeralStorageMb !== void 0 && { ephemeralStorageMb }
|
|
45565
|
+
};
|
|
45566
|
+
}
|
|
45567
|
+
/**
|
|
44603
45568
|
* Locate the Lambda's local code directory using the CDK-blessed
|
|
44604
45569
|
* `Metadata['aws:asset:path']` hint. Bind-mounted directly at
|
|
44605
45570
|
* `/var/task` (read-only) by the docker-runner.
|
|
@@ -44631,7 +45596,7 @@ function printRouteTable(routes) {
|
|
|
44631
45596
|
process.stdout.write("Discovered routes:\n");
|
|
44632
45597
|
for (const r of sorted) {
|
|
44633
45598
|
const sourceLabel = r.source === "http-api" ? "HTTP API" : r.source === "rest-v1" ? `REST v1, stage '${r.stage}'` : "Function URL";
|
|
44634
|
-
const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.lambdaLogicalId;
|
|
45599
|
+
const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.serviceIntegration ? `[${r.serviceIntegration.subtype}]` : r.lambdaLogicalId;
|
|
44635
45600
|
process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${target} (${sourceLabel})\n`);
|
|
44636
45601
|
}
|
|
44637
45602
|
process.stdout.write("\n");
|
|
@@ -48129,7 +49094,7 @@ function reorderArgs(argv) {
|
|
|
48129
49094
|
*/
|
|
48130
49095
|
async function main() {
|
|
48131
49096
|
const program = new Command();
|
|
48132
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
49097
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.129.0");
|
|
48133
49098
|
program.addCommand(createBootstrapCommand());
|
|
48134
49099
|
program.addCommand(createSynthCommand());
|
|
48135
49100
|
program.addCommand(createListCommand());
|