@go-to-k/cdkd 0.131.0 → 0.132.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 CHANGED
@@ -40433,20 +40433,74 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
40433
40433
  };
40434
40434
  const integrationType = integration["Type"];
40435
40435
  if (integrationType === "MOCK") {
40436
- const preflight = httpMethod === "OPTIONS" ? extractRestV1MockCorsConfig(integration) : void 0;
40437
- if (preflight) return [{
40436
+ if (httpMethod === "OPTIONS") {
40437
+ const preflight = extractRestV1MockCorsConfig(integration);
40438
+ if (preflight) return [{
40439
+ ...baseRoute,
40440
+ method: "OPTIONS",
40441
+ pathPattern: path,
40442
+ lambdaLogicalId: "",
40443
+ mockCors: preflight
40444
+ }];
40445
+ }
40446
+ const config = buildMockIntegrationConfig(integration);
40447
+ return [{
40448
+ ...baseRoute,
40449
+ method: httpMethod,
40450
+ pathPattern: path,
40451
+ lambdaLogicalId: "",
40452
+ restV1Integration: config
40453
+ }];
40454
+ }
40455
+ if (integrationType === "HTTP_PROXY") {
40456
+ const config = buildHttpProxyIntegrationConfig(integration, stackName, logicalId);
40457
+ if (config.kind === "unsupported") return [{
40458
+ ...baseRoute,
40459
+ method: httpMethod,
40460
+ pathPattern: path,
40461
+ lambdaLogicalId: "",
40462
+ unsupported: { reason: config.reason }
40463
+ }];
40464
+ return [{
40465
+ ...baseRoute,
40466
+ method: httpMethod,
40467
+ pathPattern: path,
40468
+ lambdaLogicalId: "",
40469
+ restV1Integration: config.config
40470
+ }];
40471
+ }
40472
+ if (integrationType === "HTTP") {
40473
+ const config = buildHttpIntegrationConfig(integration, stackName, logicalId);
40474
+ if (config.kind === "unsupported") return [{
40438
40475
  ...baseRoute,
40439
- method: "OPTIONS",
40476
+ method: httpMethod,
40440
40477
  pathPattern: path,
40441
40478
  lambdaLogicalId: "",
40442
- mockCors: preflight
40479
+ unsupported: { reason: config.reason }
40443
40480
  }];
40444
40481
  return [{
40445
40482
  ...baseRoute,
40446
40483
  method: httpMethod,
40447
40484
  pathPattern: path,
40448
40485
  lambdaLogicalId: "",
40449
- unsupported: { reason: `${stackName}/${logicalId}: MOCK integration is not emulated (only the CORS preflight subset, where HttpMethod=OPTIONS and IntegrationResponses carry literal method.response.header.* values, is supported).` }
40486
+ restV1Integration: config.config
40487
+ }];
40488
+ }
40489
+ if (integrationType === "AWS") {
40490
+ const config = buildAwsIntegrationConfig(integration, stackName, logicalId);
40491
+ if (config.kind === "unsupported") return [{
40492
+ ...baseRoute,
40493
+ method: httpMethod,
40494
+ pathPattern: path,
40495
+ lambdaLogicalId: "",
40496
+ unsupported: { reason: config.reason }
40497
+ }];
40498
+ return [{
40499
+ ...baseRoute,
40500
+ method: httpMethod,
40501
+ pathPattern: path,
40502
+ lambdaLogicalId: config.config.lambdaLogicalId,
40503
+ restV1Integration: config.config
40450
40504
  }];
40451
40505
  }
40452
40506
  if (integrationType !== "AWS_PROXY") return [{
@@ -40454,7 +40508,7 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
40454
40508
  method: httpMethod,
40455
40509
  pathPattern: path,
40456
40510
  lambdaLogicalId: "",
40457
- unsupported: { reason: `${stackName}/${logicalId}: REST v1 integration type '${String(integrationType)}' is not supported (only AWS_PROXY and the MOCK CORS preflight subset).` }
40511
+ unsupported: { reason: `${stackName}/${logicalId}: unknown REST v1 integration type '${String(integrationType)}' (expected AWS_PROXY / AWS / HTTP / HTTP_PROXY / MOCK).` }
40458
40512
  }];
40459
40513
  const integrationUri = integration["Uri"];
40460
40514
  const arnOutcome = resolveLambdaArnOutcome(integrationUri);
@@ -40524,6 +40578,188 @@ function extractRestV1MockCorsConfig(integration) {
40524
40578
  };
40525
40579
  }
40526
40580
  /**
40581
+ * Marker sequence on a Lambda invoke ARN — used to tell apart REST v1
40582
+ * `AWS` integrations whose backend is Lambda (`functions/<arn>/invocations`)
40583
+ * from other AWS-service integrations (`:s3:path/...`, `:sqs:path/...`).
40584
+ * Closes #457's AWS-vs-Lambda discrimination.
40585
+ */
40586
+ const LAMBDA_INVOKE_PATH = ":lambda:path/2015-03-31/functions/";
40587
+ /**
40588
+ * Build a MOCK integration config for `cdkd local start-api` dispatch.
40589
+ *
40590
+ * Pulls `Integration.RequestTemplates['application/json']` (drives MOCK
40591
+ * status-code selection — AWS reads `{"statusCode": N}` from the rendered
40592
+ * template) and `Integration.IntegrationResponses[]` (drives the shaped
40593
+ * response).
40594
+ */
40595
+ function buildMockIntegrationConfig(integration) {
40596
+ const requestTemplate = pickStringFromRecord(integration["RequestTemplates"], "application/json");
40597
+ const responses = readIntegrationResponses(integration);
40598
+ return {
40599
+ kind: "mock",
40600
+ requestTemplate: requestTemplate ?? void 0,
40601
+ responses
40602
+ };
40603
+ }
40604
+ /**
40605
+ * Build a HTTP_PROXY integration config. The Uri must be a literal
40606
+ * string at template-author time — `Fn::Sub` shapes with literal
40607
+ * placeholders are rare and unsupported in v1 (surfaces as a 501 with
40608
+ * a clear reason).
40609
+ */
40610
+ function buildHttpProxyIntegrationConfig(integration, stackName, logicalId) {
40611
+ const uri = integration["Uri"];
40612
+ if (typeof uri !== "string" || uri.length === 0) return {
40613
+ kind: "unsupported",
40614
+ reason: `${stackName}/${logicalId}: HTTP_PROXY Integration.Uri must be a literal string in v1 (cdkd local start-api does not resolve Fn::Sub / Fn::Join in HTTP_PROXY Uris); got ${shortJson$1(uri)}.`
40615
+ };
40616
+ const integrationHttpMethod = pickStringField(integration, "IntegrationHttpMethod");
40617
+ const requestParameters = pickStringRecord(integration["RequestParameters"]);
40618
+ const responses = readIntegrationResponses(integration);
40619
+ return {
40620
+ kind: "config",
40621
+ config: {
40622
+ kind: "http-proxy",
40623
+ uri,
40624
+ ...integrationHttpMethod !== void 0 && { integrationHttpMethod },
40625
+ ...requestParameters !== void 0 && { requestParameters },
40626
+ responses
40627
+ }
40628
+ };
40629
+ }
40630
+ /**
40631
+ * Build an HTTP (non-proxy) integration config. Like HTTP_PROXY but with
40632
+ * `RequestTemplates` for VTL transformation.
40633
+ */
40634
+ function buildHttpIntegrationConfig(integration, stackName, logicalId) {
40635
+ const uri = integration["Uri"];
40636
+ if (typeof uri !== "string" || uri.length === 0) return {
40637
+ kind: "unsupported",
40638
+ reason: `${stackName}/${logicalId}: HTTP Integration.Uri must be a literal string in v1 (cdkd local start-api does not resolve Fn::Sub / Fn::Join in HTTP Uris); got ${shortJson$1(uri)}.`
40639
+ };
40640
+ const integrationHttpMethod = pickStringField(integration, "IntegrationHttpMethod");
40641
+ const requestParameters = pickStringRecord(integration["RequestParameters"]);
40642
+ const requestTemplates = pickStringRecord(integration["RequestTemplates"]);
40643
+ const responses = readIntegrationResponses(integration);
40644
+ return {
40645
+ kind: "config",
40646
+ config: {
40647
+ kind: "http",
40648
+ uri,
40649
+ ...integrationHttpMethod !== void 0 && { integrationHttpMethod },
40650
+ ...requestParameters !== void 0 && { requestParameters },
40651
+ ...requestTemplates !== void 0 && { requestTemplates },
40652
+ responses
40653
+ }
40654
+ };
40655
+ }
40656
+ /**
40657
+ * Build an AWS integration config. Branches on whether the integration
40658
+ * targets a Lambda (`:lambda:path/2015-03-31/functions/<arn>/invocations`)
40659
+ * or a non-Lambda AWS service (`:s3:path/...` / `:sqs:action/...` etc.).
40660
+ *
40661
+ * cdkd v1 supports Lambda non-proxy AWS integrations end-to-end. Non-
40662
+ * Lambda AWS service integrations surface as deferred-501 unsupported
40663
+ * routes — they would require an AWS SDK client per service, IAM
40664
+ * credential threading, and a sizable per-service unit-test matrix.
40665
+ * See [docs/local-emulation.md](docs/local-emulation.md) for the deferred
40666
+ * AWS-service-action list.
40667
+ */
40668
+ function buildAwsIntegrationConfig(integration, stackName, logicalId) {
40669
+ const uri = integration["Uri"];
40670
+ if (!uriContainsLambdaMarker(uri)) return {
40671
+ kind: "unsupported",
40672
+ reason: `${stackName}/${logicalId}: REST v1 AWS integration targeting a non-Lambda service (Uri ${shortJson$1(uri)}) is not emulated locally in cdkd v1. Lambda non-proxy AWS integrations are supported; direct AWS service integrations (S3 / SQS / SNS / DynamoDB) require deploying to AWS. See docs/local-emulation.md.`
40673
+ };
40674
+ const arnOutcome = resolveLambdaArnOutcome(uri);
40675
+ if (arnOutcome.kind === "unsupported") return {
40676
+ kind: "unsupported",
40677
+ reason: `${stackName}/${logicalId}.Integration.Uri: ${arnOutcome.detail} (got ${shortJson$1(uri)}). Lambda Arn intrinsics on cross-stack / imported references are not resolvable locally.`
40678
+ };
40679
+ const requestTemplates = pickStringRecord(integration["RequestTemplates"]);
40680
+ const responses = readIntegrationResponses(integration);
40681
+ return {
40682
+ kind: "config",
40683
+ config: {
40684
+ kind: "aws-lambda",
40685
+ lambdaLogicalId: arnOutcome.logicalId,
40686
+ ...requestTemplates !== void 0 && { requestTemplates },
40687
+ responses
40688
+ }
40689
+ };
40690
+ }
40691
+ /**
40692
+ * Determine whether an `Integration.Uri` references a Lambda invoke path.
40693
+ * Recognises the canonical Lambda invoke ARN shape across the same
40694
+ * intrinsic forms `intrinsic-lambda-arn.ts` accepts (the shared resolver
40695
+ * never produces this Boolean directly, so we walk the shape here).
40696
+ */
40697
+ function uriContainsLambdaMarker(uri) {
40698
+ if (typeof uri === "string") return uri.includes(LAMBDA_INVOKE_PATH);
40699
+ if (uri && typeof uri === "object" && !Array.isArray(uri)) {
40700
+ const obj = uri;
40701
+ if ("Fn::Sub" in obj) {
40702
+ const v = obj["Fn::Sub"];
40703
+ if (typeof v === "string") return v.includes(LAMBDA_INVOKE_PATH);
40704
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0].includes(LAMBDA_INVOKE_PATH);
40705
+ }
40706
+ if ("Fn::Join" in obj) {
40707
+ const join = obj["Fn::Join"];
40708
+ if (Array.isArray(join) && join.length === 2 && Array.isArray(join[1])) {
40709
+ for (const piece of join[1]) if (typeof piece === "string" && piece.includes(LAMBDA_INVOKE_PATH)) return true;
40710
+ }
40711
+ }
40712
+ if ("Ref" in obj || "Fn::GetAtt" in obj) return true;
40713
+ }
40714
+ return false;
40715
+ }
40716
+ /**
40717
+ * Read `Integration.IntegrationResponses[]` from a Method's Integration
40718
+ * sub-object and return the entries cdkd's dispatchers consume.
40719
+ *
40720
+ * Defensive: rejects non-object entries with a clear inline warning.
40721
+ */
40722
+ function readIntegrationResponses(integration) {
40723
+ const raw = integration["IntegrationResponses"];
40724
+ if (!Array.isArray(raw)) return [];
40725
+ const out = [];
40726
+ for (const entry of raw) {
40727
+ if (!entry || typeof entry !== "object") continue;
40728
+ const obj = entry;
40729
+ const statusCode = obj["StatusCode"];
40730
+ if (statusCode === void 0) continue;
40731
+ if (typeof statusCode !== "string" && typeof statusCode !== "number") continue;
40732
+ const e = { StatusCode: String(statusCode) };
40733
+ if (typeof obj["SelectionPattern"] === "string") e.SelectionPattern = obj["SelectionPattern"];
40734
+ const responseParameters = pickStringRecord(obj["ResponseParameters"]);
40735
+ if (responseParameters !== void 0) e.ResponseParameters = responseParameters;
40736
+ const responseTemplates = pickStringRecord(obj["ResponseTemplates"]);
40737
+ if (responseTemplates !== void 0) e.ResponseTemplates = responseTemplates;
40738
+ if (typeof obj["ContentHandling"] === "string") e.ContentHandling = obj["ContentHandling"];
40739
+ out.push(e);
40740
+ }
40741
+ return out;
40742
+ }
40743
+ function pickStringField(props, key) {
40744
+ const v = props[key];
40745
+ return typeof v === "string" ? v : void 0;
40746
+ }
40747
+ function pickStringRecord(value) {
40748
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
40749
+ const out = {};
40750
+ let any = false;
40751
+ for (const [k, v] of Object.entries(value)) if (typeof v === "string") {
40752
+ out[k] = v;
40753
+ any = true;
40754
+ }
40755
+ return any ? out : void 0;
40756
+ }
40757
+ function pickStringFromRecord(value, key) {
40758
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
40759
+ const v = value[key];
40760
+ return typeof v === "string" ? v : void 0;
40761
+ }
40762
+ /**
40527
40763
  * Walk a chain of `AWS::ApiGateway::Resource` parent pointers up to the
40528
40764
  * `RestApi` root to build the full path. Each `Resource` contributes a
40529
40765
  * `PathPart` segment; the `RestApi` itself contributes the leading `/`.
@@ -40891,6 +41127,1354 @@ function shortJson$1(value) {
40891
41127
  }
40892
41128
  }
40893
41129
 
41130
+ //#endregion
41131
+ //#region src/local/vtl-engine.ts
41132
+ /** Error thrown when a template references an unsupported VTL feature. */
41133
+ var VtlEvaluationError = class VtlEvaluationError extends Error {
41134
+ constructor(message) {
41135
+ super(message);
41136
+ this.name = "VtlEvaluationError";
41137
+ Object.setPrototypeOf(this, VtlEvaluationError.prototype);
41138
+ }
41139
+ };
41140
+ /** Built-in `$util` implementation. */
41141
+ function buildDefaultUtil() {
41142
+ const coerce = (v) => {
41143
+ if (v == null) return "";
41144
+ if (typeof v === "string") return v;
41145
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
41146
+ try {
41147
+ return JSON.stringify(v);
41148
+ } catch {
41149
+ return "";
41150
+ }
41151
+ };
41152
+ return {
41153
+ escapeJavaScript(input) {
41154
+ return coerce(input).replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
41155
+ },
41156
+ base64Encode(input) {
41157
+ return Buffer.from(coerce(input), "utf-8").toString("base64");
41158
+ },
41159
+ base64Decode(input) {
41160
+ return Buffer.from(coerce(input), "base64").toString("utf-8");
41161
+ },
41162
+ urlEncode(input) {
41163
+ return encodeURIComponent(coerce(input));
41164
+ },
41165
+ urlDecode(input) {
41166
+ try {
41167
+ return decodeURIComponent(coerce(input));
41168
+ } catch {
41169
+ return coerce(input);
41170
+ }
41171
+ },
41172
+ parseJson(input) {
41173
+ const s = coerce(input);
41174
+ try {
41175
+ return JSON.parse(s);
41176
+ } catch (err) {
41177
+ throw new VtlEvaluationError(`$util.parseJson: invalid JSON input: ${err instanceof Error ? err.message : String(err)}`);
41178
+ }
41179
+ }
41180
+ };
41181
+ }
41182
+ /**
41183
+ * Public entry point — evaluate a VTL template against a context and
41184
+ * return the rendered string. Throws {@link VtlEvaluationError} on any
41185
+ * unsupported syntax or runtime failure.
41186
+ *
41187
+ * Empty / undefined templates short-circuit to an empty string, matching
41188
+ * AWS API Gateway behavior when `RequestTemplates` / `ResponseTemplates`
41189
+ * is absent for the selected content type.
41190
+ */
41191
+ function evaluateVtl(template, ctx) {
41192
+ if (template === void 0 || template.length === 0) return "";
41193
+ return new VtlEvaluator(ctx).evaluate(template);
41194
+ }
41195
+ /**
41196
+ * Stateful evaluator. Tokenizes + parses + renders in one pass — minimal
41197
+ * subset, so a recursive-descent walk over the template suffices. Tracks
41198
+ * a per-template scope chain for `#set` and `#foreach` bindings.
41199
+ */
41200
+ var VtlEvaluator = class {
41201
+ ctx;
41202
+ scopes;
41203
+ output = [];
41204
+ constructor(ctx) {
41205
+ this.ctx = ctx;
41206
+ this.scopes = [/* @__PURE__ */ new Map()];
41207
+ }
41208
+ evaluate(template) {
41209
+ this.renderBlock(template);
41210
+ return this.output.join("");
41211
+ }
41212
+ /**
41213
+ * Render a block — walks the template, interpolating `${var}` /
41214
+ * `$var.field.method(args)` and handling `#set` / `#if` / `#foreach`
41215
+ * directives.
41216
+ *
41217
+ * The walk is line-aware for directives: every `#directive` MUST start
41218
+ * a line (after whitespace) per Velocity convention, but for ergonomics
41219
+ * we also accept directives at the start of the template. Inline `$var`
41220
+ * references are handled anywhere.
41221
+ */
41222
+ renderBlock(block) {
41223
+ let i = 0;
41224
+ while (i < block.length) {
41225
+ const ch = block[i];
41226
+ if (ch === "#" && this.isDirectiveStart(block, i)) {
41227
+ i = this.handleDirective(block, i);
41228
+ continue;
41229
+ }
41230
+ if (ch === "$") {
41231
+ const consumed = this.handleVariable(block, i);
41232
+ if (consumed > 0) {
41233
+ i += consumed;
41234
+ continue;
41235
+ }
41236
+ }
41237
+ if (ch === "\\" && i + 1 < block.length && block[i + 1] === "$") {
41238
+ this.output.push("$");
41239
+ i += 2;
41240
+ continue;
41241
+ }
41242
+ this.output.push(ch ?? "");
41243
+ i++;
41244
+ }
41245
+ }
41246
+ isDirectiveStart(block, i) {
41247
+ if (i + 1 >= block.length) return false;
41248
+ const next = block[i + 1];
41249
+ if (next === "#") return true;
41250
+ return next !== void 0 && /[a-zA-Z]/.test(next);
41251
+ }
41252
+ /**
41253
+ * Handle one directive (`#set`, `#if`, `#foreach`, etc.) — returns the
41254
+ * NEW index in `block` (i.e. how far we consumed past the directive).
41255
+ */
41256
+ handleDirective(block, start) {
41257
+ if (block[start + 1] === "#") {
41258
+ const eol = block.indexOf("\n", start);
41259
+ return eol === -1 ? block.length : eol + 1;
41260
+ }
41261
+ const directiveMatch = /^#([a-zA-Z]+)/.exec(block.slice(start));
41262
+ if (!directiveMatch) {
41263
+ this.output.push("#");
41264
+ return start + 1;
41265
+ }
41266
+ const name = directiveMatch[1];
41267
+ const afterDirective = start + 1 + name.length;
41268
+ switch (name) {
41269
+ case "set": return this.handleSetDirective(block, afterDirective);
41270
+ case "if": return this.handleIfDirective(block, afterDirective);
41271
+ case "foreach": return this.handleForeachDirective(block, afterDirective);
41272
+ case "else":
41273
+ case "elseif":
41274
+ case "end": throw new VtlEvaluationError(`Unexpected #${name} outside of a #if / #foreach block`);
41275
+ default: throw new VtlEvaluationError(`Unsupported VTL directive #${name} (cdkd local start-api supports #set / #if / #elseif / #else / #foreach / #end / ##)`);
41276
+ }
41277
+ }
41278
+ /**
41279
+ * `#set($var = expression)` — assigns to the innermost scope.
41280
+ */
41281
+ handleSetDirective(block, after) {
41282
+ const { args, end } = this.readParenArgs(block, after);
41283
+ const eq = args.indexOf("=");
41284
+ if (eq === -1) throw new VtlEvaluationError(`#set requires '=': got #set(${args})`);
41285
+ const left = args.slice(0, eq).trim();
41286
+ const right = args.slice(eq + 1).trim();
41287
+ if (!left.startsWith("$")) throw new VtlEvaluationError(`#set left side must be a $var reference (got '${left}')`);
41288
+ const varName = left.slice(1).replace(/^\{/, "").replace(/\}$/, "");
41289
+ const value = this.evaluateExpression(right);
41290
+ this.scopes[this.scopes.length - 1].set(varName, value);
41291
+ return this.skipDirectiveTrailingNewline(block, end);
41292
+ }
41293
+ /**
41294
+ * `#if (cond) ... #elseif (cond) ... #else ... #end`. Renders the
41295
+ * first true branch only; the rest are skipped (their text NOT emitted).
41296
+ */
41297
+ handleIfDirective(block, after) {
41298
+ const { args: condExpr, end } = this.readParenArgs(block, after);
41299
+ let rendered = false;
41300
+ let renderedAny = false;
41301
+ const branches = [{
41302
+ condition: condExpr,
41303
+ bodyStart: this.skipDirectiveTrailingNewline(block, end),
41304
+ bodyEnd: -1
41305
+ }];
41306
+ let cursor = branches[0].bodyStart;
41307
+ let depth = 1;
41308
+ while (cursor < block.length && depth > 0) {
41309
+ if (block[cursor] !== "#") {
41310
+ cursor++;
41311
+ continue;
41312
+ }
41313
+ const m = /^#([a-zA-Z]+)/.exec(block.slice(cursor));
41314
+ if (!m) {
41315
+ cursor++;
41316
+ continue;
41317
+ }
41318
+ const tag = m[1];
41319
+ const tagAfter = cursor + 1 + tag.length;
41320
+ if (tag === "if" || tag === "foreach") {
41321
+ depth++;
41322
+ cursor = tagAfter;
41323
+ continue;
41324
+ }
41325
+ if (tag === "end") {
41326
+ depth--;
41327
+ if (depth === 0) {
41328
+ branches[branches.length - 1].bodyEnd = cursor;
41329
+ const endIdx = this.skipDirectiveTrailingNewline(block, tagAfter);
41330
+ for (const branch of branches) {
41331
+ if (rendered) break;
41332
+ const truthy = branch.condition === null ? !renderedAny : this.evaluateCondition(branch.condition);
41333
+ if (truthy) {
41334
+ this.renderBlock(block.slice(branch.bodyStart, branch.bodyEnd));
41335
+ rendered = true;
41336
+ }
41337
+ renderedAny = renderedAny || truthy;
41338
+ }
41339
+ return endIdx;
41340
+ }
41341
+ cursor = tagAfter;
41342
+ continue;
41343
+ }
41344
+ if (depth === 1 && (tag === "elseif" || tag === "else")) {
41345
+ branches[branches.length - 1].bodyEnd = cursor;
41346
+ if (tag === "elseif") {
41347
+ const { args, end: elseifEnd } = this.readParenArgs(block, tagAfter);
41348
+ branches.push({
41349
+ condition: args,
41350
+ bodyStart: this.skipDirectiveTrailingNewline(block, elseifEnd),
41351
+ bodyEnd: -1
41352
+ });
41353
+ cursor = branches[branches.length - 1].bodyStart;
41354
+ } else {
41355
+ branches.push({
41356
+ condition: null,
41357
+ bodyStart: this.skipDirectiveTrailingNewline(block, tagAfter),
41358
+ bodyEnd: -1
41359
+ });
41360
+ cursor = branches[branches.length - 1].bodyStart;
41361
+ }
41362
+ continue;
41363
+ }
41364
+ cursor = tagAfter;
41365
+ }
41366
+ throw new VtlEvaluationError("#if without matching #end");
41367
+ }
41368
+ /**
41369
+ * `#foreach($x in $list) ... #end` — iterates a list / object's values.
41370
+ */
41371
+ handleForeachDirective(block, after) {
41372
+ const { args, end } = this.readParenArgs(block, after);
41373
+ const m = /^\s*\$([a-zA-Z_][a-zA-Z_0-9]*)\s+in\s+(.+)$/.exec(args);
41374
+ if (!m) throw new VtlEvaluationError(`Invalid #foreach syntax: ${args}`);
41375
+ const varName = m[1];
41376
+ const listExpr = m[2];
41377
+ const listValue = this.evaluateExpression(listExpr);
41378
+ let depth = 1;
41379
+ let cursor = this.skipDirectiveTrailingNewline(block, end);
41380
+ const bodyStart = cursor;
41381
+ while (cursor < block.length && depth > 0) {
41382
+ if (block[cursor] !== "#") {
41383
+ cursor++;
41384
+ continue;
41385
+ }
41386
+ const tm = /^#([a-zA-Z]+)/.exec(block.slice(cursor));
41387
+ if (!tm) {
41388
+ cursor++;
41389
+ continue;
41390
+ }
41391
+ const tag = tm[1];
41392
+ if (tag === "if" || tag === "foreach") {
41393
+ depth++;
41394
+ cursor += 1 + tag.length;
41395
+ continue;
41396
+ }
41397
+ if (tag === "end") {
41398
+ depth--;
41399
+ if (depth === 0) {
41400
+ const bodyEnd = cursor;
41401
+ const endIdx = this.skipDirectiveTrailingNewline(block, cursor + 1 + tag.length);
41402
+ const items = this.coerceToIterable(listValue);
41403
+ for (const item of items) {
41404
+ this.scopes.push(new Map([[varName, item]]));
41405
+ try {
41406
+ this.renderBlock(block.slice(bodyStart, bodyEnd));
41407
+ } finally {
41408
+ this.scopes.pop();
41409
+ }
41410
+ }
41411
+ return endIdx;
41412
+ }
41413
+ }
41414
+ cursor += 1 + tag.length;
41415
+ }
41416
+ throw new VtlEvaluationError("#foreach without matching #end");
41417
+ }
41418
+ /** Convert a value into an iterable sequence for `#foreach`. */
41419
+ coerceToIterable(value) {
41420
+ if (Array.isArray(value)) return value;
41421
+ if (value && typeof value === "object") return Object.values(value);
41422
+ if (value == null) return [];
41423
+ return [value];
41424
+ }
41425
+ /**
41426
+ * Skip whitespace and a single trailing newline immediately after a
41427
+ * directive — matches Velocity's "directive eats its own newline"
41428
+ * convention. Without this rule, every `#set(...)` line in a template
41429
+ * would leave a blank line in the output.
41430
+ */
41431
+ skipDirectiveTrailingNewline(block, after) {
41432
+ let i = after;
41433
+ while (i < block.length && (block[i] === " " || block[i] === " ")) i++;
41434
+ if (block[i] === "\r") i++;
41435
+ if (block[i] === "\n") i++;
41436
+ return i;
41437
+ }
41438
+ /**
41439
+ * Read `(...)` arguments after a directive name. Returns the inner
41440
+ * string + the index AFTER the closing paren. Handles nested parens
41441
+ * inside string literals / method calls.
41442
+ */
41443
+ readParenArgs(block, after) {
41444
+ let i = after;
41445
+ while (i < block.length && (block[i] === " " || block[i] === " ")) i++;
41446
+ if (block[i] !== "(") throw new VtlEvaluationError(`Expected '(' after directive at offset ${after}`);
41447
+ i++;
41448
+ let depth = 1;
41449
+ const start = i;
41450
+ let inString = null;
41451
+ while (i < block.length && depth > 0) {
41452
+ const c = block[i];
41453
+ if (inString) {
41454
+ if (c === "\\" && i + 1 < block.length) {
41455
+ i += 2;
41456
+ continue;
41457
+ }
41458
+ if (c === inString) inString = null;
41459
+ i++;
41460
+ continue;
41461
+ }
41462
+ if (c === "\"" || c === "'") {
41463
+ inString = c;
41464
+ i++;
41465
+ continue;
41466
+ }
41467
+ if (c === "(") depth++;
41468
+ else if (c === ")") depth--;
41469
+ if (depth === 0) break;
41470
+ i++;
41471
+ }
41472
+ if (depth !== 0) throw new VtlEvaluationError(`Unterminated parenthesised argument at offset ${after}`);
41473
+ return {
41474
+ args: block.slice(start, i),
41475
+ end: i + 1
41476
+ };
41477
+ }
41478
+ /**
41479
+ * Handle a `$var` / `${var}` / `$obj.field.method(args)` reference.
41480
+ * Returns the number of characters consumed (0 if not a reference —
41481
+ * caller emits the literal `$`).
41482
+ */
41483
+ handleVariable(block, start) {
41484
+ const m = /^\$(\{[^}]+\}|[a-zA-Z_][a-zA-Z_0-9]*(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*)/.exec(block.slice(start));
41485
+ if (!m) return 0;
41486
+ const ref = m[1];
41487
+ const refStr = ref.startsWith("{") ? ref.slice(1, -1) : ref;
41488
+ let consumed = m[0].length;
41489
+ let value = this.resolveReference(refStr);
41490
+ let pos = start + consumed;
41491
+ while (pos < block.length) {
41492
+ if (block[pos] === "(") {
41493
+ const { args, end } = this.readParenArgs(block, pos);
41494
+ value = this.callValueAsMethod(value, args, refStr);
41495
+ consumed = end - start;
41496
+ pos = end;
41497
+ if (pos < block.length && block[pos] === ".") {
41498
+ const tailMatch = /^\.([a-zA-Z_][a-zA-Z_0-9]*)/.exec(block.slice(pos));
41499
+ if (tailMatch) {
41500
+ const field = tailMatch[1];
41501
+ value = lookupField(value, field);
41502
+ consumed += tailMatch[0].length;
41503
+ pos += tailMatch[0].length;
41504
+ continue;
41505
+ }
41506
+ }
41507
+ break;
41508
+ }
41509
+ break;
41510
+ }
41511
+ this.output.push(this.stringifyForOutput(value));
41512
+ return consumed;
41513
+ }
41514
+ /**
41515
+ * Resolve a dotted reference path against context + scopes. The first
41516
+ * segment is matched against built-in roots (`input` / `context` / `util`
41517
+ * / `inputRoot`) and the scope chain in order.
41518
+ */
41519
+ resolveReference(path) {
41520
+ const parts = path.split(".");
41521
+ const first = parts[0];
41522
+ const rest = parts.slice(1);
41523
+ let base;
41524
+ if (first === "input") base = this.ctx.input;
41525
+ else if (first === "context") base = this.ctx.context;
41526
+ else if (first === "util") base = this.ctx.util;
41527
+ else if (first === "inputRoot") base = this.ctx.inputRoot;
41528
+ else {
41529
+ let found = false;
41530
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
41531
+ const scope = this.scopes[i];
41532
+ if (scope.has(first)) {
41533
+ base = scope.get(first);
41534
+ found = true;
41535
+ break;
41536
+ }
41537
+ }
41538
+ if (!found) return null;
41539
+ }
41540
+ return rest.reduce((acc, seg) => lookupField(acc, seg), base);
41541
+ }
41542
+ /**
41543
+ * Invoke a value as a method — used after a `$ref(args)` shape. The
41544
+ * value must be a function or a special-cased built-in.
41545
+ */
41546
+ callValueAsMethod(value, argsRaw, refPath) {
41547
+ if (typeof value !== "function") throw new VtlEvaluationError(`Reference '$${refPath}' is not callable (got ${typeof value}). cdkd supports calling $input / $util / $context method-style references only.`);
41548
+ return value(...this.parseArgList(argsRaw));
41549
+ }
41550
+ /**
41551
+ * Parse a comma-separated argument list — recursively evaluates each
41552
+ * expression. Handles string literals, numbers, booleans, and nested
41553
+ * `$var` refs.
41554
+ */
41555
+ parseArgList(raw) {
41556
+ const trimmed = raw.trim();
41557
+ if (trimmed.length === 0) return [];
41558
+ const parts = [];
41559
+ let depth = 0;
41560
+ let inString = null;
41561
+ let start = 0;
41562
+ for (let i = 0; i < trimmed.length; i++) {
41563
+ const c = trimmed[i];
41564
+ if (inString) {
41565
+ if (c === "\\" && i + 1 < trimmed.length) {
41566
+ i++;
41567
+ continue;
41568
+ }
41569
+ if (c === inString) inString = null;
41570
+ continue;
41571
+ }
41572
+ if (c === "\"" || c === "'") {
41573
+ inString = c;
41574
+ continue;
41575
+ }
41576
+ if (c === "(" || c === "[") depth++;
41577
+ else if (c === ")" || c === "]") depth--;
41578
+ else if (c === "," && depth === 0) {
41579
+ parts.push(trimmed.slice(start, i));
41580
+ start = i + 1;
41581
+ }
41582
+ }
41583
+ parts.push(trimmed.slice(start));
41584
+ return parts.map((p) => this.evaluateExpression(p.trim()));
41585
+ }
41586
+ /**
41587
+ * Evaluate a sub-expression (string literal / number / boolean / null /
41588
+ * `$ref` / `$ref.field`). Tiny grammar — no arithmetic operators.
41589
+ */
41590
+ evaluateExpression(expr) {
41591
+ const trimmed = expr.trim();
41592
+ if (trimmed.length === 0) return null;
41593
+ if (trimmed === "true") return true;
41594
+ if (trimmed === "false") return false;
41595
+ if (trimmed === "null") return null;
41596
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return this.unescapeStringLiteral(trimmed.slice(1, -1));
41597
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
41598
+ if (trimmed.startsWith("$")) {
41599
+ const refMatch = /^\$(\{[^}]+\}|[a-zA-Z_][a-zA-Z_0-9]*(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*)$/.exec(trimmed);
41600
+ if (refMatch) {
41601
+ const refStr = refMatch[1];
41602
+ const refPath = refStr.startsWith("{") ? refStr.slice(1, -1) : refStr;
41603
+ return this.resolveReference(refPath);
41604
+ }
41605
+ const callMatch = /^\$([a-zA-Z_][a-zA-Z_0-9]*(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*)\((.*)\)$/.exec(trimmed);
41606
+ if (callMatch) {
41607
+ const refPath = callMatch[1];
41608
+ const argsRaw = callMatch[2];
41609
+ const value = this.resolveReference(refPath);
41610
+ return this.callValueAsMethod(value, argsRaw, refPath);
41611
+ }
41612
+ }
41613
+ throw new VtlEvaluationError(`Could not evaluate VTL sub-expression: '${trimmed}'`);
41614
+ }
41615
+ unescapeStringLiteral(s) {
41616
+ return s.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\"/g, "\"").replace(/\\'/g, "'");
41617
+ }
41618
+ /**
41619
+ * Evaluate a `#if` / `#elseif` condition expression. Supports `&&`,
41620
+ * `||`, `!`, comparison ops, and bare value tests (truthy/falsy).
41621
+ */
41622
+ evaluateCondition(expr) {
41623
+ const trimmed = expr.trim();
41624
+ const orParts = splitTopLevel(trimmed, "||");
41625
+ if (orParts.length > 1) return orParts.some((p) => this.evaluateCondition(p));
41626
+ const andParts = splitTopLevel(trimmed, "&&");
41627
+ if (andParts.length > 1) return andParts.every((p) => this.evaluateCondition(p));
41628
+ if (trimmed.startsWith("!")) return !this.evaluateCondition(trimmed.slice(1).trim());
41629
+ if (trimmed.startsWith("(") && trimmed.endsWith(")")) return this.evaluateCondition(trimmed.slice(1, -1));
41630
+ for (const op of [
41631
+ "==",
41632
+ "!=",
41633
+ "<=",
41634
+ ">=",
41635
+ "<",
41636
+ ">"
41637
+ ]) {
41638
+ const parts = splitTopLevel(trimmed, op);
41639
+ if (parts.length === 2) return compareValues(this.evaluateExpression(parts[0]), this.evaluateExpression(parts[1]), op);
41640
+ }
41641
+ return isTruthy(this.evaluateExpression(trimmed));
41642
+ }
41643
+ /**
41644
+ * Convert a value to its template output form. Mirrors Velocity's
41645
+ * `toString` convention: `null` → empty string; objects → JSON; numbers
41646
+ * / booleans → standard.
41647
+ */
41648
+ stringifyForOutput(value) {
41649
+ if (value === null || value === void 0) return "";
41650
+ if (typeof value === "string") return value;
41651
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
41652
+ return JSON.stringify(value);
41653
+ }
41654
+ };
41655
+ /**
41656
+ * Look up `field` on `obj`. Returns `null` for missing fields (Velocity
41657
+ * silent-undefined convention).
41658
+ */
41659
+ function lookupField(obj, field) {
41660
+ if (obj == null) return null;
41661
+ if (typeof obj === "object") {
41662
+ const rec = obj;
41663
+ if (Object.prototype.hasOwnProperty.call(rec, field)) return rec[field];
41664
+ return null;
41665
+ }
41666
+ return null;
41667
+ }
41668
+ function splitTopLevel(s, sep) {
41669
+ const out = [];
41670
+ let depth = 0;
41671
+ let inString = null;
41672
+ let start = 0;
41673
+ for (let i = 0; i < s.length; i++) {
41674
+ const c = s[i];
41675
+ if (inString) {
41676
+ if (c === "\\" && i + 1 < s.length) {
41677
+ i++;
41678
+ continue;
41679
+ }
41680
+ if (c === inString) inString = null;
41681
+ continue;
41682
+ }
41683
+ if (c === "\"" || c === "'") {
41684
+ inString = c;
41685
+ continue;
41686
+ }
41687
+ if (c === "(" || c === "[") depth++;
41688
+ else if (c === ")" || c === "]") depth--;
41689
+ else if (depth === 0 && s.startsWith(sep, i)) {
41690
+ out.push(s.slice(start, i));
41691
+ start = i + sep.length;
41692
+ i += sep.length - 1;
41693
+ }
41694
+ }
41695
+ out.push(s.slice(start));
41696
+ return out;
41697
+ }
41698
+ function compareValues(lhs, rhs, op) {
41699
+ if (op === "==") return looseEqual(lhs, rhs);
41700
+ if (op === "!=") return !looseEqual(lhs, rhs);
41701
+ const a = typeof lhs === "number" ? lhs : Number(lhs);
41702
+ const b = typeof rhs === "number" ? rhs : Number(rhs);
41703
+ if (Number.isFinite(a) && Number.isFinite(b)) switch (op) {
41704
+ case "<": return a < b;
41705
+ case "<=": return a <= b;
41706
+ case ">": return a > b;
41707
+ case ">=": return a >= b;
41708
+ }
41709
+ const sa = String(lhs);
41710
+ const sb = String(rhs);
41711
+ switch (op) {
41712
+ case "<": return sa < sb;
41713
+ case "<=": return sa <= sb;
41714
+ case ">": return sa > sb;
41715
+ case ">=": return sa >= sb;
41716
+ }
41717
+ }
41718
+ function looseEqual(a, b) {
41719
+ if (a === b) return true;
41720
+ if (a == null || b == null) return a == null && b == null;
41721
+ if (typeof a === typeof b) {
41722
+ if (typeof a === "object") return JSON.stringify(a) === JSON.stringify(b);
41723
+ return false;
41724
+ }
41725
+ return safeStringify(a) === safeStringify(b);
41726
+ }
41727
+ function safeStringify(v) {
41728
+ if (v == null) return "";
41729
+ if (typeof v === "string") return v;
41730
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
41731
+ try {
41732
+ return JSON.stringify(v);
41733
+ } catch {
41734
+ return "";
41735
+ }
41736
+ }
41737
+ function isTruthy(v) {
41738
+ if (v == null) return false;
41739
+ if (typeof v === "boolean") return v;
41740
+ if (typeof v === "number") return v !== 0;
41741
+ if (typeof v === "string") return v.length > 0;
41742
+ if (Array.isArray(v)) return v.length > 0;
41743
+ if (typeof v === "object") return Object.keys(v).length > 0;
41744
+ return true;
41745
+ }
41746
+ /**
41747
+ * Build a `VtlInput` binding from an HTTP request snapshot + matched
41748
+ * route context. `$input` exposes the body + parameter accessors used by
41749
+ * AWS API Gateway's VTL templates.
41750
+ *
41751
+ * `params()` returns the union of header / querystring / path maps;
41752
+ * `params(name)` resolves first against path, then querystring, then
41753
+ * header (matches AWS-deployed precedence).
41754
+ *
41755
+ * `json(jsonPath)` returns a JSON-stringified slice of the parsed body;
41756
+ * `path(jsonPath)` returns the raw native value (primitives unquoted).
41757
+ *
41758
+ * JSONPath support is minimal: supports `$` (root), `$.field`,
41759
+ * `$.field.subField`, `$.array[0]`. AWS supports more (filter
41760
+ * expressions, recursive descent); cdkd surfaces a clear error on
41761
+ * unsupported expressions rather than silently producing wrong output.
41762
+ */
41763
+ function buildVtlInput(body, headers, querystring, pathParams) {
41764
+ let jsonBodyCache;
41765
+ let jsonBodyParsed = false;
41766
+ function lazyJson() {
41767
+ if (!jsonBodyParsed) {
41768
+ jsonBodyParsed = true;
41769
+ try {
41770
+ jsonBodyCache = body.length === 0 ? null : JSON.parse(body);
41771
+ } catch {
41772
+ jsonBodyCache = null;
41773
+ }
41774
+ }
41775
+ return jsonBodyCache;
41776
+ }
41777
+ function jsonFn(...args) {
41778
+ const expr = args.length > 0 ? String(args[0]) : "$";
41779
+ const val = applyJsonPath(lazyJson(), expr);
41780
+ return JSON.stringify(val ?? null);
41781
+ }
41782
+ function pathFn(...args) {
41783
+ const expr = args.length > 0 ? String(args[0]) : "$";
41784
+ return applyJsonPath(lazyJson(), expr);
41785
+ }
41786
+ function paramsFn(...args) {
41787
+ if (args.length === 0) return {
41788
+ header: headers,
41789
+ querystring,
41790
+ path: pathParams
41791
+ };
41792
+ const arg = String(args[0]);
41793
+ if (arg === "header") return headers;
41794
+ if (arg === "querystring") return querystring;
41795
+ if (arg === "path") return pathParams;
41796
+ if (Object.prototype.hasOwnProperty.call(pathParams, arg)) return pathParams[arg];
41797
+ if (Object.prototype.hasOwnProperty.call(querystring, arg)) return querystring[arg];
41798
+ if (Object.prototype.hasOwnProperty.call(headers, arg)) return headers[arg];
41799
+ const lowerArg = arg.toLowerCase();
41800
+ for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === lowerArg) return v;
41801
+ return null;
41802
+ }
41803
+ return {
41804
+ body,
41805
+ get jsonBody() {
41806
+ return lazyJson();
41807
+ },
41808
+ headers,
41809
+ querystring,
41810
+ path: pathParams,
41811
+ json: jsonFn,
41812
+ path: pathFn,
41813
+ params: paramsFn
41814
+ };
41815
+ }
41816
+ /**
41817
+ * Minimal JSONPath evaluator. Supports `$`, `$.field`, `$.field.sub`,
41818
+ * `$.array[index]`. Unsupported syntax throws so the user sees a clear
41819
+ * pointer to the gap.
41820
+ */
41821
+ function applyJsonPath(root, expr) {
41822
+ const trimmed = expr.trim();
41823
+ if (trimmed === "$" || trimmed.length === 0) return root;
41824
+ if (!trimmed.startsWith("$")) throw new VtlEvaluationError(`JSONPath must start with '$': got '${trimmed}'`);
41825
+ let cursor = root;
41826
+ let i = 1;
41827
+ while (i < trimmed.length) {
41828
+ const c = trimmed[i];
41829
+ if (c === ".") {
41830
+ i++;
41831
+ const m = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(trimmed.slice(i));
41832
+ if (!m) throw new VtlEvaluationError(`Unsupported JSONPath syntax at position ${i}: '${trimmed}' (cdkd supports $, $.field, $.field.sub, $.array[index] only).`);
41833
+ cursor = lookupField(cursor, m[0]);
41834
+ i += m[0].length;
41835
+ continue;
41836
+ }
41837
+ if (c === "[") {
41838
+ const close = trimmed.indexOf("]", i);
41839
+ if (close === -1) throw new VtlEvaluationError(`Unterminated [ in JSONPath: '${trimmed}'`);
41840
+ const inside = trimmed.slice(i + 1, close).trim();
41841
+ if (/^-?\d+$/.test(inside)) {
41842
+ const idx = Number(inside);
41843
+ if (Array.isArray(cursor)) cursor = cursor[idx];
41844
+ else cursor = null;
41845
+ } else if (inside.startsWith("\"") && inside.endsWith("\"") || inside.startsWith("'") && inside.endsWith("'")) cursor = lookupField(cursor, inside.slice(1, -1));
41846
+ else throw new VtlEvaluationError(`Unsupported JSONPath bracket expression: '${inside}' (cdkd supports integer indices and quoted string keys only).`);
41847
+ i = close + 1;
41848
+ continue;
41849
+ }
41850
+ throw new VtlEvaluationError(`Unexpected character in JSONPath at position ${i}: '${trimmed}'`);
41851
+ }
41852
+ return cursor;
41853
+ }
41854
+ /**
41855
+ * Build a `$context` binding from a request snapshot + matched route.
41856
+ * The mapping mirrors what AWS API Gateway exposes (see AWS docs:
41857
+ * https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html).
41858
+ */
41859
+ function buildVtlRequestContext(args) {
41860
+ return {
41861
+ requestId: args.requestId,
41862
+ httpMethod: args.httpMethod,
41863
+ resourcePath: args.resourcePath,
41864
+ stage: args.stage,
41865
+ identity: {
41866
+ sourceIp: args.sourceIp,
41867
+ userAgent: args.userAgent
41868
+ }
41869
+ };
41870
+ }
41871
+
41872
+ //#endregion
41873
+ //#region src/local/integration-response-selector.ts
41874
+ /**
41875
+ * Pick the right `IntegrationResponses[]` entry for the given outcome.
41876
+ *
41877
+ * Per AWS docs, `SelectionPattern` is matched against the backend
41878
+ * outcome regardless of whether the backend returned success or error —
41879
+ * a `SelectionPattern: '200'` entry IS expected to match an HTTP 200
41880
+ * upstream response. cdkd ALWAYS runs the regex loop first and only
41881
+ * falls to the default entry when no pattern matches; pre-#505-review
41882
+ * the success branch short-circuited to the default entry without
41883
+ * running the regex loop, which silently dropped success-side selection.
41884
+ *
41885
+ * @param entries - The `IntegrationResponses[]` array from the template
41886
+ * (already extracted from the route's Integration property).
41887
+ * @param matchTarget - The string AWS would match `SelectionPattern`
41888
+ * against. For HTTP / HTTP_PROXY this is `String(upstream.status)`;
41889
+ * for Lambda this is the `errorMessage` field on the parsed payload,
41890
+ * or the sentinel `'success'` when the payload has no `errorMessage`.
41891
+ * For MOCK this is unused (MOCK dispatch picks by `StatusCode`).
41892
+ * @param fallbackStatusCode - Status code to use when `entries` is empty
41893
+ * or no entry matches AND no default entry exists. HTTP / HTTP_PROXY
41894
+ * pass the upstream status; Lambda passes 200 on success / 500 on
41895
+ * error.
41896
+ */
41897
+ function selectIntegrationResponse(entries, matchTarget, fallbackStatusCode = 200) {
41898
+ if (!entries || entries.length === 0) return {
41899
+ entry: null,
41900
+ statusCode: fallbackStatusCode
41901
+ };
41902
+ const defaultEntry = entries.find((e) => e.SelectionPattern === void 0 || e.SelectionPattern === "");
41903
+ for (const entry of entries) {
41904
+ if (entry.SelectionPattern === void 0 || entry.SelectionPattern === "") continue;
41905
+ try {
41906
+ if (new RegExp(`^${entry.SelectionPattern}$`).test(matchTarget)) return {
41907
+ entry,
41908
+ statusCode: parseStatus$1(entry.StatusCode, fallbackStatusCode)
41909
+ };
41910
+ } catch {}
41911
+ }
41912
+ const entry = defaultEntry ?? null;
41913
+ return {
41914
+ entry,
41915
+ statusCode: entry !== null ? parseStatus$1(entry.StatusCode, fallbackStatusCode) : fallbackStatusCode
41916
+ };
41917
+ }
41918
+ function parseStatus$1(raw, fallback) {
41919
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
41920
+ if (typeof raw === "string") {
41921
+ const parsed = Number.parseInt(raw, 10);
41922
+ if (Number.isFinite(parsed)) return parsed;
41923
+ }
41924
+ return fallback;
41925
+ }
41926
+ /**
41927
+ * Evaluate `IntegrationResponse.ResponseParameters` — header literals
41928
+ * mapped onto the HTTP response. Returns `{name: value}` for every entry
41929
+ * we could resolve; unresolvable entries (non-literal / mapping
41930
+ * expression) get a warning via `onUnsupported` and are skipped.
41931
+ *
41932
+ * AWS format: keys are `method.response.header.<HeaderName>`; values
41933
+ * are `'literal'` (with single quotes) or mapping expressions
41934
+ * (`integration.response.body.X` / `integration.response.header.X` /
41935
+ * `context.X`). cdkd v1 supports the literal form only.
41936
+ */
41937
+ function evaluateResponseParameters(responseParameters, opts = {}) {
41938
+ if (!responseParameters) return {};
41939
+ const out = {};
41940
+ for (const [key, value] of Object.entries(responseParameters)) {
41941
+ const headerMatch = /^method\.response\.header\.(.+)$/.exec(key);
41942
+ if (!headerMatch) {
41943
+ opts.onUnsupported?.(key, value, `Only method.response.header.<name> keys are supported on REST v1 ResponseParameters; cdkd cannot map ${key}.`);
41944
+ continue;
41945
+ }
41946
+ const headerName = headerMatch[1];
41947
+ if (typeof value !== "string") {
41948
+ opts.onUnsupported?.(key, String(value), `non-string ResponseParameter value`);
41949
+ continue;
41950
+ }
41951
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
41952
+ out[headerName] = value.slice(1, -1);
41953
+ continue;
41954
+ }
41955
+ opts.onUnsupported?.(key, value, `ResponseParameter value '${value}' is a mapping expression (integration.response.* / context.*) which cdkd local start-api does not emulate. Only single-quoted literals are honored.`);
41956
+ }
41957
+ return out;
41958
+ }
41959
+ /**
41960
+ * Pick the response template AWS would render for the given Accept
41961
+ * header. AWS uses content negotiation; cdkd picks `application/json`
41962
+ * first, then any other entry. Returns `undefined` when no template is
41963
+ * configured (caller emits the backend body verbatim).
41964
+ *
41965
+ * The chosen template's content-type is also returned so the dispatcher
41966
+ * can emit a matching `Content-Type` header (matches AWS-deployed
41967
+ * behavior).
41968
+ */
41969
+ function pickResponseTemplate(responseTemplates, accept) {
41970
+ if (!responseTemplates) return void 0;
41971
+ const entries = Object.entries(responseTemplates);
41972
+ if (entries.length === 0) return void 0;
41973
+ if (accept) {
41974
+ const acceptTypes = accept.split(",").map((s) => s.split(";")[0].trim()).filter(Boolean);
41975
+ for (const acceptType of acceptTypes) for (const [ct, template] of entries) if (ct === acceptType) return {
41976
+ template,
41977
+ contentType: ct
41978
+ };
41979
+ }
41980
+ const jsonEntry = responseTemplates["application/json"];
41981
+ if (jsonEntry !== void 0) return {
41982
+ template: jsonEntry,
41983
+ contentType: "application/json"
41984
+ };
41985
+ const first = entries[0];
41986
+ return {
41987
+ template: first[1],
41988
+ contentType: first[0]
41989
+ };
41990
+ }
41991
+
41992
+ //#endregion
41993
+ //#region src/local/rest-v1-integrations.ts
41994
+ /**
41995
+ * Dispatch a MOCK integration. AWS MOCK semantics:
41996
+ *
41997
+ * 1. Render `RequestTemplates['application/json']` (VTL) against the
41998
+ * request — yields a JSON object like `{"statusCode": 200}`.
41999
+ * 2. Parse the rendered JSON; pick the `IntegrationResponses[]` entry
42000
+ * whose `StatusCode` equals the parsed `statusCode` (string compare,
42001
+ * mirroring AWS).
42002
+ * 3. Render the picked entry's `ResponseTemplates[<content-type>]`
42003
+ * against an empty body context and emit it.
42004
+ * 4. Apply `ResponseParameters` header literals.
42005
+ *
42006
+ * When no request template is configured AWS defaults to picking the
42007
+ * `IntegrationResponses[]` entry with `SelectionPattern === ''` (or the
42008
+ * first entry).
42009
+ */
42010
+ function dispatchMockIntegration(config, req) {
42011
+ const logger = getLogger().child("start-api");
42012
+ const ctx = buildVtlContextFromRequest(req, "");
42013
+ let pickedStatus;
42014
+ if (config.requestTemplate !== void 0 && config.requestTemplate.trim().length > 0) try {
42015
+ pickedStatus = extractStatusCodeFromRendered(evaluateVtl(config.requestTemplate, ctx));
42016
+ } catch (err) {
42017
+ return vtlFailure("request", err, config.requestTemplate);
42018
+ }
42019
+ let entry = null;
42020
+ if (pickedStatus !== void 0) entry = config.responses.find((e) => parseStatus(e.StatusCode) === pickedStatus) ?? defaultResponseEntry(config.responses);
42021
+ else entry = defaultResponseEntry(config.responses);
42022
+ if (!entry) return {
42023
+ statusCode: pickedStatus ?? 200,
42024
+ headers: { "content-type": "application/json" },
42025
+ body: ""
42026
+ };
42027
+ const accept = req.headers["accept"];
42028
+ const picked = pickResponseTemplate(entry.ResponseTemplates, accept);
42029
+ const respCtx = buildVtlContextFromRequest(req, "", null);
42030
+ let body = "";
42031
+ let contentType = "application/json";
42032
+ if (picked) {
42033
+ try {
42034
+ body = evaluateVtl(picked.template, respCtx);
42035
+ } catch (err) {
42036
+ return vtlFailure("response", err, picked.template);
42037
+ }
42038
+ contentType = picked.contentType;
42039
+ }
42040
+ const headers = { "content-type": contentType };
42041
+ Object.assign(headers, evaluateResponseParameters(entry.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`MOCK response: ${reason}`) }));
42042
+ return {
42043
+ statusCode: parseStatus(entry.StatusCode) ?? 200,
42044
+ headers,
42045
+ body
42046
+ };
42047
+ }
42048
+ /**
42049
+ * Dispatch an HTTP_PROXY integration. The request is forwarded verbatim
42050
+ * with `RequestParameters` mappings applied; the response is also
42051
+ * forwarded verbatim (AWS does NOT apply ResponseTemplates on HTTP_PROXY,
42052
+ * only IntegrationResponses[].SelectionPattern routes the status code).
42053
+ */
42054
+ async function dispatchHttpProxyIntegration(config, req, deps) {
42055
+ const url = substituteUriPlaceholders(config.uri, req);
42056
+ const method = config.integrationHttpMethod ?? req.method;
42057
+ const outHeaders = { ...req.headers };
42058
+ for (const drop of [
42059
+ "host",
42060
+ "connection",
42061
+ "content-length",
42062
+ "transfer-encoding"
42063
+ ]) delete outHeaders[drop];
42064
+ applyRequestParameters(config.requestParameters, req, {
42065
+ headers: outHeaders,
42066
+ urlObj: void 0
42067
+ });
42068
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
42069
+ const fetchInit = {
42070
+ method,
42071
+ headers: outHeaders
42072
+ };
42073
+ if (req.body.length > 0) fetchInit.body = new Uint8Array(req.body);
42074
+ let upstream;
42075
+ try {
42076
+ upstream = await fetchImpl(url, fetchInit);
42077
+ } catch (err) {
42078
+ return {
42079
+ statusCode: 502,
42080
+ headers: { "content-type": "application/json" },
42081
+ body: JSON.stringify({
42082
+ message: "HTTP_PROXY upstream unreachable",
42083
+ url,
42084
+ reason: err instanceof Error ? err.message : String(err)
42085
+ })
42086
+ };
42087
+ }
42088
+ const upstreamBody = Buffer.from(await upstream.arrayBuffer());
42089
+ const selected = selectIntegrationResponse(config.responses, String(upstream.status), upstream.status);
42090
+ const headers = {};
42091
+ upstream.headers.forEach((value, name) => {
42092
+ headers[name.toLowerCase()] = value;
42093
+ });
42094
+ delete headers["content-encoding"];
42095
+ delete headers["content-length"];
42096
+ const logger = getLogger().child("start-api");
42097
+ Object.assign(headers, evaluateResponseParameters(selected.entry?.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`HTTP_PROXY response: ${reason}`) }));
42098
+ return {
42099
+ statusCode: selected.entry ? selected.statusCode : upstream.status,
42100
+ headers,
42101
+ body: upstreamBody
42102
+ };
42103
+ }
42104
+ /**
42105
+ * Dispatch an HTTP (non-proxy) integration: HTTP_PROXY + VTL on both
42106
+ * directions. Same upstream-call shape; the request body is transformed
42107
+ * via VTL, and the response body is transformed via VTL too.
42108
+ */
42109
+ async function dispatchHttpIntegration(config, req, deps) {
42110
+ const logger = getLogger().child("start-api");
42111
+ const url = substituteUriPlaceholders(config.uri, req);
42112
+ const method = config.integrationHttpMethod ?? req.method;
42113
+ const ctx = buildVtlContextFromRequest(req, req.body.toString("utf-8"));
42114
+ const reqTemplate = pickRequestTemplate(config.requestTemplates, req.headers["content-type"]);
42115
+ let outBody;
42116
+ let outContentType = req.headers["content-type"] ?? "application/json";
42117
+ if (reqTemplate) {
42118
+ try {
42119
+ outBody = evaluateVtl(reqTemplate.template, ctx);
42120
+ } catch (err) {
42121
+ return vtlFailure("request", err, reqTemplate.template);
42122
+ }
42123
+ outContentType = reqTemplate.contentType;
42124
+ } else outBody = req.body.toString("utf-8");
42125
+ const outHeaders = {
42126
+ ...req.headers,
42127
+ "content-type": outContentType
42128
+ };
42129
+ for (const drop of [
42130
+ "host",
42131
+ "connection",
42132
+ "content-length",
42133
+ "transfer-encoding"
42134
+ ]) delete outHeaders[drop];
42135
+ applyRequestParameters(config.requestParameters, req, {
42136
+ headers: outHeaders,
42137
+ urlObj: void 0
42138
+ });
42139
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
42140
+ const fetchInit = {
42141
+ method,
42142
+ headers: outHeaders
42143
+ };
42144
+ if (outBody !== void 0 && outBody.length > 0) fetchInit.body = outBody;
42145
+ let upstream;
42146
+ try {
42147
+ upstream = await fetchImpl(url, fetchInit);
42148
+ } catch (err) {
42149
+ return {
42150
+ statusCode: 502,
42151
+ headers: { "content-type": "application/json" },
42152
+ body: JSON.stringify({
42153
+ message: "HTTP upstream unreachable",
42154
+ url,
42155
+ reason: err instanceof Error ? err.message : String(err)
42156
+ })
42157
+ };
42158
+ }
42159
+ const upstreamContentType = upstream.headers.get("content-type") ?? "application/octet-stream";
42160
+ const isUpstreamTextLike = isTextLikeContentType(upstreamContentType);
42161
+ let upstreamText;
42162
+ let upstreamBinary;
42163
+ if (isUpstreamTextLike) upstreamText = await upstream.text();
42164
+ else upstreamBinary = Buffer.from(await upstream.arrayBuffer());
42165
+ const selected = selectIntegrationResponse(config.responses, String(upstream.status), upstream.status);
42166
+ let body;
42167
+ let contentType = upstreamContentType;
42168
+ if (selected.entry) {
42169
+ const picked = pickResponseTemplate(selected.entry.ResponseTemplates, req.headers["accept"]);
42170
+ if (picked) if (upstreamText === void 0) {
42171
+ logger.warn(`HTTP response: ResponseTemplates set but upstream Content-Type '${upstreamContentType}' is binary; passing body through unchanged.`);
42172
+ body = upstreamBinary;
42173
+ } else {
42174
+ const respCtx = buildVtlContextFromRequest(req, upstreamText, safeJsonParse(upstreamText));
42175
+ try {
42176
+ body = evaluateVtl(picked.template, respCtx);
42177
+ } catch (err) {
42178
+ return vtlFailure("response", err, picked.template);
42179
+ }
42180
+ contentType = picked.contentType;
42181
+ }
42182
+ else body = upstreamText ?? upstreamBinary;
42183
+ } else body = upstreamText ?? upstreamBinary;
42184
+ const headers = { "content-type": contentType };
42185
+ Object.assign(headers, evaluateResponseParameters(selected.entry?.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`HTTP response: ${reason}`) }));
42186
+ return {
42187
+ statusCode: selected.statusCode,
42188
+ headers,
42189
+ body
42190
+ };
42191
+ }
42192
+ /**
42193
+ * Dispatch an AWS (Lambda non-proxy) integration. The request body is
42194
+ * transformed via VTL into the Lambda event; the Lambda is invoked via
42195
+ * RIE; the return value is transformed via ResponseTemplates.
42196
+ *
42197
+ * AWS error routing: when the Lambda returns an object with an
42198
+ * `errorMessage` field (Node Lambda runtime convention), AWS treats it
42199
+ * as an error and matches `SelectionPattern` against the
42200
+ * `errorMessage`. Otherwise success.
42201
+ */
42202
+ async function dispatchAwsLambdaIntegration(config, req, deps) {
42203
+ const logger = getLogger().child("start-api");
42204
+ const ctx = buildVtlContextFromRequest(req, req.body.toString("utf-8"));
42205
+ const template = pickRequestTemplate(config.requestTemplates, req.headers["content-type"]);
42206
+ let eventPayload;
42207
+ if (template) {
42208
+ let rendered;
42209
+ try {
42210
+ rendered = evaluateVtl(template.template, ctx);
42211
+ } catch (err) {
42212
+ return vtlFailure("request", err, template.template);
42213
+ }
42214
+ try {
42215
+ eventPayload = JSON.parse(rendered);
42216
+ } catch {
42217
+ eventPayload = rendered;
42218
+ }
42219
+ } else eventPayload = safeJsonParse(req.body.toString("utf-8")) ?? req.body.toString("utf-8");
42220
+ let handle;
42221
+ try {
42222
+ handle = await deps.pool.acquire(config.lambdaLogicalId);
42223
+ } catch (err) {
42224
+ return {
42225
+ statusCode: 502,
42226
+ headers: { "content-type": "application/json" },
42227
+ body: JSON.stringify({
42228
+ message: "Failed to acquire RIE container for AWS Lambda non-proxy integration",
42229
+ reason: err instanceof Error ? err.message : String(err)
42230
+ })
42231
+ };
42232
+ }
42233
+ let invokeOutcome;
42234
+ try {
42235
+ invokeOutcome = await invokeRie(handle.containerHost, handle.hostPort, eventPayload, deps.rieTimeoutMs);
42236
+ } catch (err) {
42237
+ deps.pool.release(handle);
42238
+ return {
42239
+ statusCode: 502,
42240
+ headers: { "content-type": "application/json" },
42241
+ body: JSON.stringify({
42242
+ message: "AWS Lambda non-proxy invocation failed",
42243
+ reason: err instanceof Error ? err.message : String(err)
42244
+ })
42245
+ };
42246
+ }
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
+ }
42274
+ function buildVtlContextFromRequest(req, body, inputRoot) {
42275
+ return {
42276
+ input: buildVtlInput(body, req.headers, req.querystring, req.pathParameters),
42277
+ context: buildVtlRequestContext({
42278
+ requestId: req.requestId,
42279
+ httpMethod: req.method,
42280
+ resourcePath: req.resourcePath,
42281
+ stage: req.stage,
42282
+ sourceIp: req.sourceIp,
42283
+ userAgent: req.userAgent
42284
+ }),
42285
+ util: buildDefaultUtil(),
42286
+ ...inputRoot !== void 0 && { inputRoot }
42287
+ };
42288
+ }
42289
+ function pickRequestTemplate(requestTemplates, contentType) {
42290
+ if (!requestTemplates) return void 0;
42291
+ const entries = Object.entries(requestTemplates);
42292
+ if (entries.length === 0) return void 0;
42293
+ if (contentType) {
42294
+ const primary = contentType.split(";")[0].trim();
42295
+ if (requestTemplates[primary] !== void 0) return {
42296
+ template: requestTemplates[primary],
42297
+ contentType: primary
42298
+ };
42299
+ }
42300
+ if (requestTemplates["application/json"] !== void 0) return {
42301
+ template: requestTemplates["application/json"],
42302
+ contentType: "application/json"
42303
+ };
42304
+ const first = entries[0];
42305
+ return {
42306
+ template: first[1],
42307
+ contentType: first[0]
42308
+ };
42309
+ }
42310
+ /**
42311
+ * Extract `{"statusCode": <N>}` from a rendered MOCK request template.
42312
+ * AWS uses this single key to drive `IntegrationResponses[]` selection.
42313
+ */
42314
+ function extractStatusCodeFromRendered(rendered) {
42315
+ try {
42316
+ const parsed = JSON.parse(rendered);
42317
+ if (parsed && typeof parsed === "object" && "statusCode" in parsed) {
42318
+ const val = parsed["statusCode"];
42319
+ if (typeof val === "number") return val;
42320
+ if (typeof val === "string") {
42321
+ const n = Number.parseInt(val, 10);
42322
+ if (Number.isFinite(n)) return n;
42323
+ }
42324
+ }
42325
+ } catch {}
42326
+ }
42327
+ function defaultResponseEntry(entries) {
42328
+ return entries.find((e) => e.SelectionPattern === void 0 || e.SelectionPattern === "") ?? null;
42329
+ }
42330
+ function parseStatus(raw) {
42331
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
42332
+ if (typeof raw === "string") {
42333
+ const n = Number.parseInt(raw, 10);
42334
+ if (Number.isFinite(n)) return n;
42335
+ }
42336
+ }
42337
+ /**
42338
+ * Heuristic: is the given HTTP `Content-Type` header value likely to
42339
+ * carry text content that VTL ResponseTemplates can safely render
42340
+ * against? Used by `dispatchHttpIntegration` to branch the upstream
42341
+ * body read between `.text()` (text-like) and `.arrayBuffer()` (binary
42342
+ * pass-through). Charset parameters are stripped before matching.
42343
+ *
42344
+ * Exported for unit testing.
42345
+ */
42346
+ function isTextLikeContentType(contentType) {
42347
+ const primary = contentType.split(";")[0].trim().toLowerCase();
42348
+ if (primary.startsWith("text/")) return true;
42349
+ if (primary === "application/json" || primary === "application/xml" || primary === "application/x-www-form-urlencoded" || primary === "application/javascript" || primary === "application/ld+json") return true;
42350
+ if (primary.startsWith("application/") && (primary.endsWith("+json") || primary.endsWith("+xml"))) return true;
42351
+ return false;
42352
+ }
42353
+ /**
42354
+ * Classify a hostname or IP literal against well-known internal address
42355
+ * spaces. Used by `warnSsrfRiskyUri` at server boot to surface a warn
42356
+ * line per HTTP / HTTP_PROXY integration whose URI points at a
42357
+ * potentially-sensitive destination. Best-effort; does NOT do DNS
42358
+ * resolution — only matches hostname literals that are already an IP.
42359
+ *
42360
+ * Returns `undefined` when the host appears safe (public DNS name) OR
42361
+ * cannot be classified (DNS name that may resolve to an internal IP
42362
+ * the helper cannot see without async DNS).
42363
+ *
42364
+ * Exported for unit testing.
42365
+ */
42366
+ function classifyInternalHost(host) {
42367
+ const h = host.replace(/^\[|\]$/g, "");
42368
+ if (h === "169.254.169.254" || h === "[fd00:ec2::254]" || h === "fd00:ec2::254") return "AWS IMDS (169.254.169.254) — credentials exfiltration risk";
42369
+ if (/^127\.\d+\.\d+\.\d+$/.test(h)) return "IPv4 loopback (127.0.0.0/8)";
42370
+ if (h === "::1") return "IPv6 loopback (::1)";
42371
+ if (/^169\.254\.\d+\.\d+$/.test(h)) return "IPv4 link-local (169.254.0.0/16)";
42372
+ if (/^fe[89ab][0-9a-f]?:/i.test(h)) return "IPv6 link-local (fe80::/10)";
42373
+ if (/^10\.\d+\.\d+\.\d+$/.test(h)) return "RFC1918 private (10.0.0.0/8)";
42374
+ if (/^192\.168\.\d+\.\d+$/.test(h)) return "RFC1918 private (192.168.0.0/16)";
42375
+ const m = /^172\.(\d+)\.\d+\.\d+$/.exec(h);
42376
+ if (m && Number(m[1]) >= 16 && Number(m[1]) <= 31) return "RFC1918 private (172.16.0.0/12)";
42377
+ }
42378
+ /**
42379
+ * Emit a `logger.warn` line for each HTTP / HTTP_PROXY integration
42380
+ * whose `Integration.Uri` parses to a hostname classified as internal
42381
+ * by `classifyInternalHost`. Called once at server boot from
42382
+ * `cdkd local start-api`'s discovery pass; per-route deduplicated.
42383
+ *
42384
+ * cdkd does NOT block the URI — this is a developer-loop tool, not a
42385
+ * security boundary, and warn-and-proceed matches the precedent set by
42386
+ * the cognito JWKS pass-through fallback. The right v2 follow-up is an
42387
+ * `--allow-internal-uri` flag (and an opposite default block) once the
42388
+ * surface is well-understood.
42389
+ */
42390
+ function warnSsrfRiskyUri(uri, routeLabel, warn) {
42391
+ let host;
42392
+ try {
42393
+ const sanitized = uri.replace(/\{[^/{}]+\}/g, "x");
42394
+ host = new URL(sanitized).hostname;
42395
+ } catch {
42396
+ return;
42397
+ }
42398
+ const classification = classifyInternalHost(host);
42399
+ if (classification !== void 0) warn(`Integration URI for ${routeLabel} points at ${host} — ${classification}. cdkd does NOT block this; ensure the upstream is intentional.`);
42400
+ }
42401
+ function safeJsonParse(s) {
42402
+ try {
42403
+ return JSON.parse(s);
42404
+ } catch {
42405
+ return null;
42406
+ }
42407
+ }
42408
+ /**
42409
+ * Apply `Integration.RequestParameters` mappings — header / query / path
42410
+ * rewrites that copy from `method.request.X` to `integration.request.Y`.
42411
+ *
42412
+ * Supported key shapes:
42413
+ * - `integration.request.header.<name>` → outgoing header
42414
+ * - `integration.request.querystring.<name>` → query string param
42415
+ * - `integration.request.path.<name>` → path placeholder substitution
42416
+ *
42417
+ * Supported value shapes:
42418
+ * - `method.request.header.<name>` → read incoming header
42419
+ * - `method.request.querystring.<name>` → read incoming query param
42420
+ * - `method.request.path.<name>` → read path parameter
42421
+ * - `'literal'` → single-quoted literal
42422
+ *
42423
+ * Unsupported mapping expressions are logged at warn and skipped (matches
42424
+ * the ResponseParameters handling in `integration-response-selector.ts`).
42425
+ */
42426
+ function applyRequestParameters(requestParameters, req, out) {
42427
+ if (!requestParameters) return;
42428
+ const logger = getLogger().child("start-api");
42429
+ for (const [key, value] of Object.entries(requestParameters)) {
42430
+ const resolved = resolveRequestParameterValue(value, req);
42431
+ if (resolved === void 0) {
42432
+ logger.warn(`RequestParameter '${key}' value '${value}' is not a recognized mapping; skipping.`);
42433
+ continue;
42434
+ }
42435
+ const headerMatch = /^integration\.request\.header\.(.+)$/.exec(key);
42436
+ const queryMatch = /^integration\.request\.querystring\.(.+)$/.exec(key);
42437
+ const pathMatch = /^integration\.request\.path\.(.+)$/.exec(key);
42438
+ if (headerMatch) out.headers[headerMatch[1].toLowerCase()] = resolved;
42439
+ else if (queryMatch) logger.warn(`RequestParameter '${key}' (querystring rewrite) is recognized but cdkd applies querystring rewrites only via URI placeholder substitution; ignoring.`);
42440
+ else if (pathMatch) logger.warn(`RequestParameter '${key}' (path rewrite) is recognized but cdkd substitutes path placeholders via {param} in the URI; ignoring.`);
42441
+ else logger.warn(`Unsupported RequestParameter key '${key}'; skipping.`);
42442
+ }
42443
+ }
42444
+ function resolveRequestParameterValue(raw, req) {
42445
+ if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
42446
+ const headerMatch = /^method\.request\.header\.(.+)$/.exec(raw);
42447
+ if (headerMatch) return req.headers[headerMatch[1].toLowerCase()];
42448
+ const queryMatch = /^method\.request\.querystring\.(.+)$/.exec(raw);
42449
+ if (queryMatch) return req.querystring[queryMatch[1]];
42450
+ const pathMatch = /^method\.request\.path\.(.+)$/.exec(raw);
42451
+ if (pathMatch) return req.pathParameters[pathMatch[1]];
42452
+ }
42453
+ /**
42454
+ * Substitute `{paramName}` placeholders in a URI string with the value
42455
+ * of the matching path parameter on the request. Used by HTTP_PROXY /
42456
+ * HTTP integrations whose `Integration.Uri` may contain such
42457
+ * placeholders (e.g. `https://upstream.example.com/users/{userId}`).
42458
+ */
42459
+ function substituteUriPlaceholders(uri, req) {
42460
+ return uri.replace(/\{([^/{}]+)\}/g, (_, name) => {
42461
+ const val = req.pathParameters[name];
42462
+ return val !== void 0 ? encodeURIComponent(val) : "";
42463
+ });
42464
+ }
42465
+ function vtlFailure(direction, err, template) {
42466
+ const reason = err instanceof VtlEvaluationError ? err.message : err instanceof Error ? err.message : String(err);
42467
+ return {
42468
+ statusCode: 502,
42469
+ headers: { "content-type": "application/json" },
42470
+ body: JSON.stringify({
42471
+ message: `VTL ${direction}-template evaluation failed`,
42472
+ reason,
42473
+ template: template.length > 200 ? template.slice(0, 200) + "..." : template
42474
+ })
42475
+ };
42476
+ }
42477
+
40894
42478
  //#endregion
40895
42479
  //#region src/local/container-pool.ts
40896
42480
  const DEFAULT_IDLE_MS = 6e4;
@@ -43871,6 +45455,16 @@ async function handleRequest(req, res, state, opts) {
43871
45455
  const overlay = buildOverlay(authorizer, authResult);
43872
45456
  if (overlay) baseEvent = applyAuthorizerOverlay(baseEvent, overlay);
43873
45457
  }
45458
+ if (match.route.restV1Integration) {
45459
+ try {
45460
+ writeIntegrationOutcome(res, await dispatchRestV1Integration(match.route.restV1Integration, snapshot, matchCtx, state, opts));
45461
+ } catch (err) {
45462
+ logger.error(`REST v1 ${match.route.restV1Integration.kind} dispatch failed for ${match.route.declaredAt}: ${err instanceof Error ? err.message : String(err)}`);
45463
+ if (!res.headersSent) writeError(res, 502);
45464
+ else res.end();
45465
+ }
45466
+ return;
45467
+ }
43874
45468
  let handle;
43875
45469
  try {
43876
45470
  handle = await state.pool.acquire(match.route.lambdaLogicalId);
@@ -43950,6 +45544,50 @@ function writeStreamingResponse(res, result, releasePool) {
43950
45544
  body.pipe(res);
43951
45545
  }
43952
45546
  /**
45547
+ * Dispatch a REST v1 non-AWS_PROXY integration to the matching handler.
45548
+ * Built once per request from the matched route + request snapshot.
45549
+ *
45550
+ * Returns a {@link RestV1IntegrationOutcome} — the caller writes it
45551
+ * onto the `ServerResponse` via {@link writeIntegrationOutcome}.
45552
+ */
45553
+ async function dispatchRestV1Integration(integration, snapshot, matchCtx, state, opts) {
45554
+ const headers = lowercaseSingularHeaders(snapshot.headers);
45555
+ const querystring = parseQueryStringSingular(snapshot.rawUrl);
45556
+ const sourceIp = snapshot.sourceIp ?? "127.0.0.1";
45557
+ const userAgent = headers["user-agent"] ?? "";
45558
+ const req = {
45559
+ method: snapshot.method.toUpperCase(),
45560
+ matchedPath: matchCtx.matchedPath,
45561
+ pathParameters: matchCtx.pathParameters,
45562
+ querystring,
45563
+ headers,
45564
+ body: snapshot.body,
45565
+ sourceIp,
45566
+ userAgent,
45567
+ stage: matchCtx.route.stage,
45568
+ resourcePath: matchCtx.route.pathPattern,
45569
+ requestId: randomUUID()
45570
+ };
45571
+ const deps = {
45572
+ pool: state.pool,
45573
+ rieTimeoutMs: opts.rieTimeoutMs
45574
+ };
45575
+ switch (integration.kind) {
45576
+ case "mock": return dispatchMockIntegration(integration, req);
45577
+ case "http-proxy": return await dispatchHttpProxyIntegration(integration, req, deps);
45578
+ case "http": return await dispatchHttpIntegration(integration, req, deps);
45579
+ case "aws-lambda": return await dispatchAwsLambdaIntegration(integration, req, deps);
45580
+ }
45581
+ }
45582
+ /**
45583
+ * Write a {@link RestV1IntegrationOutcome} to the HTTP response.
45584
+ */
45585
+ function writeIntegrationOutcome(res, outcome) {
45586
+ res.statusCode = outcome.statusCode;
45587
+ for (const [name, value] of Object.entries(outcome.headers)) res.setHeader(name, value);
45588
+ res.end(outcome.body);
45589
+ }
45590
+ /**
43953
45591
  * Attempt CORS preflight interception. Returns `true` when the
43954
45592
  * preflight response was written (caller must NOT continue to route
43955
45593
  * dispatch); `false` when no preflight match (caller falls through to
@@ -45265,7 +46903,9 @@ async function localStartApiCommand(target, options) {
45265
46903
  if (basePort !== 0) nextPort += 1;
45266
46904
  }
45267
46905
  printPerServerRouteTables(servers);
45268
- warnUnsupportedRoutes(servers.flatMap((s) => s.group.routes.map((r) => r.route)), logger);
46906
+ const allRoutes = servers.flatMap((s) => s.group.routes.map((r) => r.route));
46907
+ warnUnsupportedRoutes(allRoutes, logger);
46908
+ warnSsrfRiskyIntegrations(allRoutes, logger);
45269
46909
  logger.info(`Per-Lambda concurrency: ${perLambdaConcurrency} (override with --per-lambda-concurrency)`);
45270
46910
  for (const { group, server } of servers) process.stdout.write(`Server listening on ${server.scheme}://${server.host}:${server.port} (${group.displayName})\n`);
45271
46911
  process.stdout.write("^C to stop and clean up containers.\n");
@@ -45814,12 +47454,26 @@ function printRouteTable(routes) {
45814
47454
  process.stdout.write("Discovered routes:\n");
45815
47455
  for (const r of sorted) {
45816
47456
  const sourceLabel = r.source === "http-api" ? "HTTP API" : r.source === "rest-v1" ? `REST v1, stage '${r.stage}'` : "Function URL";
45817
- const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.serviceIntegration ? `[${r.serviceIntegration.subtype}]` : r.lambdaLogicalId;
47457
+ const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.serviceIntegration ? `[${r.serviceIntegration.subtype}]` : r.restV1Integration ? formatRestV1IntegrationLabel(r.restV1Integration) : r.lambdaLogicalId;
45818
47458
  process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${target} (${sourceLabel})\n`);
45819
47459
  }
45820
47460
  process.stdout.write("\n");
45821
47461
  }
45822
47462
  /**
47463
+ * Format the route-table label for a REST v1 non-AWS_PROXY integration.
47464
+ * `MOCK` / `HTTP` / `HTTP_PROXY` show their integration kind directly;
47465
+ * `AWS` (Lambda non-proxy) shows the Lambda logical id with an `[AWS]`
47466
+ * suffix so it's distinguishable from AWS_PROXY rows. Closes #457.
47467
+ */
47468
+ function formatRestV1IntegrationLabel(integration) {
47469
+ switch (integration.kind) {
47470
+ case "mock": return "[MOCK]";
47471
+ case "http-proxy": return `[HTTP_PROXY ${integration.uri}]`;
47472
+ case "http": return `[HTTP ${integration.uri}]`;
47473
+ case "aws-lambda": return `${integration.lambdaLogicalId} [AWS]`;
47474
+ }
47475
+ }
47476
+ /**
45823
47477
  * Materialize an inline Lambda body (`Code.ZipFile`) to a tmpdir and
45824
47478
  * return the directory the container should mount at /var/task.
45825
47479
  * Mirrors `cdkd local invoke`'s implementation; the only divergence is
@@ -45973,6 +47627,26 @@ function warnUnsupportedRoutes(routes, logger) {
45973
47627
  return unsupported.length;
45974
47628
  }
45975
47629
  /**
47630
+ * Surface a one-line warn per HTTP / HTTP_PROXY integration whose
47631
+ * `Integration.Uri` points at a well-known internal address space
47632
+ * (AWS IMDS, loopback, link-local, RFC1918). PR #505 / issue #457
47633
+ * follow-up: cdkd does NOT block these — warn-and-proceed matches the
47634
+ * cognito JWKS pass-through pattern — but the user should see the
47635
+ * destination at boot so a malicious / typo'd template Uri does not
47636
+ * silently exfiltrate credentials in CI. Deduplicated per-Uri.
47637
+ */
47638
+ function warnSsrfRiskyIntegrations(routes, logger) {
47639
+ const seen = /* @__PURE__ */ new Set();
47640
+ for (const r of routes) {
47641
+ const integ = r.restV1Integration;
47642
+ if (!integ) continue;
47643
+ if (integ.kind !== "http" && integ.kind !== "http-proxy") continue;
47644
+ if (seen.has(integ.uri)) continue;
47645
+ seen.add(integ.uri);
47646
+ warnSsrfRiskyUri(integ.uri, `${r.method} ${r.pathPattern}`, (msg) => logger.warn(msg));
47647
+ }
47648
+ }
47649
+ /**
45976
47650
  * One reload cycle for the multi-server topology (issue #260). The
45977
47651
  * watcher serializes calls via a chain promise; this function:
45978
47652
  *
@@ -46021,7 +47695,9 @@ async function reloadAllServers(args) {
46021
47695
  lastAssetPaths.value = computeAssetPaths(material.specs);
46022
47696
  if (watcher) watcher.update([output, ...lastAssetPaths.value]);
46023
47697
  printPerServerRouteTables(servers);
46024
- warnUnsupportedRoutes(servers.flatMap((s) => s.group.routes.map((r) => r.route)), logger);
47698
+ const allRoutes = servers.flatMap((s) => s.group.routes.map((r) => r.route));
47699
+ warnUnsupportedRoutes(allRoutes, logger);
47700
+ warnSsrfRiskyIntegrations(allRoutes, logger);
46025
47701
  }
46026
47702
  /**
46027
47703
  * Returns true when any value in the function's template env map is a
@@ -50031,7 +51707,7 @@ function reorderArgs(argv) {
50031
51707
  */
50032
51708
  async function main() {
50033
51709
  const program = new Command();
50034
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.131.0");
51710
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.132.0");
50035
51711
  program.addCommand(createBootstrapCommand());
50036
51712
  program.addCommand(createSynthCommand());
50037
51713
  program.addCommand(createListCommand());