@go-to-k/cdkd 0.132.3 → 0.133.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
@@ -41767,6 +41767,23 @@ function compareValues(lhs, rhs, op) {
41767
41767
  case ">=": return sa >= sb;
41768
41768
  }
41769
41769
  }
41770
+ /**
41771
+ * `==` / `!=` semantics for the VTL evaluator. Mirrors Apache Velocity's
41772
+ * lenient equality: same-type primitives compare via `===`; nested objects
41773
+ * / arrays compare structurally via canonical JSON; cross-type pairs (the
41774
+ * common case in AWS API Gateway templates, e.g. `#if($x == "true")` where
41775
+ * `$x` is a boolean from `$input.json('$.flag')`) fall back to comparing
41776
+ * each side coerced through {@link safeStringify}. The cross-type fall-back
41777
+ * is what Velocity itself does in this context, NOT JavaScript's
41778
+ * `==` operator (which has its own well-known foot-guns around `null` /
41779
+ * `undefined` / `NaN`). See the Velocity reference for the canonical
41780
+ * semantics: <https://velocity.apache.org/engine/2.0/vtl-reference.html#a3.4.4.RelationalOperators>
41781
+ * (look for the "Equivalent" / "Not Equivalent" rows).
41782
+ *
41783
+ * Both `null` and `undefined` collapse to a single sentinel — two
41784
+ * unset variables are considered equal. This matches Velocity's lenient
41785
+ * `null` handling (Issue (#507) item 8).
41786
+ */
41770
41787
  function looseEqual(a, b) {
41771
41788
  if (a === b) return true;
41772
41789
  if (a == null || b == null) return a == null && b == null;
@@ -41807,6 +41824,19 @@ function isTruthy(v) {
41807
41824
  * `json(jsonPath)` returns a JSON-stringified slice of the parsed body;
41808
41825
  * `path(jsonPath)` returns the raw native value (primitives unquoted).
41809
41826
  *
41827
+ * Case-sensitivity contract (Issue (#507) item 5; mirrored on the write
41828
+ * side by `rest-v1-integrations.ts` `resolveRequestParameterValue` /
41829
+ * `applyRequestParameters`):
41830
+ *
41831
+ * - `$input.params('<name>')` resolves PATH (case-sensitive) →
41832
+ * QUERYSTRING (case-sensitive, last-wins on duplicates) → HEADER
41833
+ * (case-insensitive — first the input's pre-lowercased map, then a
41834
+ * last-resort case-insensitive scan; multi-value duplicates are
41835
+ * comma-joined by the http-server before reaching VTL).
41836
+ * - `$input.params('header')` / `$input.params('querystring')` /
41837
+ * `$input.params('path')` return the raw category maps without
41838
+ * case-folding the keys further.
41839
+ *
41810
41840
  * JSONPath support is minimal: supports `$` (root), `$.field`,
41811
41841
  * `$.field.subField`, `$.array[0]`. AWS supports more (filter
41812
41842
  * expressions, recursive descent); cdkd surfaces a clear error on
@@ -41815,20 +41845,24 @@ function isTruthy(v) {
41815
41845
  function buildVtlInput(body, headers, querystring, pathParams) {
41816
41846
  let jsonBodyCache;
41817
41847
  let jsonBodyParsed = false;
41818
- function lazyJson() {
41848
+ let jsonBodyParseError = false;
41849
+ function lazyJson(opts) {
41819
41850
  if (!jsonBodyParsed) {
41820
41851
  jsonBodyParsed = true;
41821
- try {
41822
- jsonBodyCache = body.length === 0 ? null : JSON.parse(body);
41852
+ if (body.length === 0) jsonBodyCache = null;
41853
+ else try {
41854
+ jsonBodyCache = JSON.parse(body);
41823
41855
  } catch {
41824
41856
  jsonBodyCache = null;
41857
+ jsonBodyParseError = true;
41825
41858
  }
41826
41859
  }
41860
+ if (jsonBodyParseError && opts?.throwOnParseError) throw new VtlEvaluationError("$input.json(...) cannot be evaluated: request body is not valid JSON. On AWS API Gateway this surface returns HTTP 400 with {\"message\":\"Invalid JSON request body\"}; use $input.body or $input.path(...) when the upstream may emit non-JSON content.");
41827
41861
  return jsonBodyCache;
41828
41862
  }
41829
41863
  function jsonFn(...args) {
41830
41864
  const expr = args.length > 0 ? String(args[0]) : "$";
41831
- const val = applyJsonPath(lazyJson(), expr);
41865
+ const val = applyJsonPath(lazyJson({ throwOnParseError: true }), expr);
41832
41866
  return JSON.stringify(val ?? null);
41833
41867
  }
41834
41868
  function pathFn(...args) {
@@ -41968,12 +42002,17 @@ function selectIntegrationResponse(entries, matchTarget, fallbackStatusCode = 20
41968
42002
  };
41969
42003
  }
41970
42004
  function parseStatus$1(raw, fallback) {
41971
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
41972
- if (typeof raw === "string") {
41973
- const parsed = Number.parseInt(raw, 10);
41974
- if (Number.isFinite(parsed)) return parsed;
41975
- }
41976
- return fallback;
42005
+ if (typeof raw === "number") {
42006
+ if (Number.isInteger(raw) && raw >= 100 && raw < 600) return raw;
42007
+ return fallback;
42008
+ }
42009
+ if (typeof raw !== "string") return fallback;
42010
+ const trimmed = raw.trim();
42011
+ if (trimmed === "") return fallback;
42012
+ const parsed = Number(trimmed);
42013
+ if (!Number.isInteger(parsed)) return fallback;
42014
+ if (parsed < 100 || parsed >= 600) return fallback;
42015
+ return parsed;
41977
42016
  }
41978
42017
  /**
41979
42018
  * Evaluate `IntegrationResponse.ResponseParameters` — header literals
@@ -41985,6 +42024,17 @@ function parseStatus$1(raw, fallback) {
41985
42024
  * are `'literal'` (with single quotes) or mapping expressions
41986
42025
  * (`integration.response.body.X` / `integration.response.header.X` /
41987
42026
  * `context.X`). cdkd v1 supports the literal form only.
42027
+ *
42028
+ * PR #511 review fix-back: header names are lowercased here so the
42029
+ * returned map shares the same key namespace as the dispatcher's
42030
+ * default-initialized headers (`{'content-type': '...'}`). Without
42031
+ * normalization a template that sets `Content-Type` PascalCase via
42032
+ * ResponseParameters produced a headers object carrying BOTH
42033
+ * `'content-type': 'application/json'` (default) AND
42034
+ * `'Content-Type': 'text/xml'` (overlay), which downstream HTTP
42035
+ * serialization rendered as two conflicting headers — AWS-deployed
42036
+ * only ever returns one. By lowercasing every key, overlays simply
42037
+ * overwrite the default-initializer entry like AWS does.
41988
42038
  */
41989
42039
  function evaluateResponseParameters(responseParameters, opts = {}) {
41990
42040
  if (!responseParameters) return {};
@@ -41995,7 +42045,7 @@ function evaluateResponseParameters(responseParameters, opts = {}) {
41995
42045
  opts.onUnsupported?.(key, value, `Only method.response.header.<name> keys are supported on REST v1 ResponseParameters; cdkd cannot map ${key}.`);
41996
42046
  continue;
41997
42047
  }
41998
- const headerName = headerMatch[1];
42048
+ const headerName = headerMatch[1].toLowerCase();
41999
42049
  if (typeof value !== "string") {
42000
42050
  opts.onUnsupported?.(key, String(value), `non-string ResponseParameter value`);
42001
42051
  continue;
@@ -42091,6 +42141,7 @@ function dispatchMockIntegration(config, req) {
42091
42141
  }
42092
42142
  const headers = { "content-type": contentType };
42093
42143
  Object.assign(headers, evaluateResponseParameters(entry.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`MOCK response: ${reason}`) }));
42144
+ if (body === "" && headers["content-type"] === contentType) delete headers["content-type"];
42094
42145
  return {
42095
42146
  statusCode: parseStatus(entry.StatusCode) ?? 200,
42096
42147
  headers,
@@ -42113,10 +42164,7 @@ async function dispatchHttpProxyIntegration(config, req, deps) {
42113
42164
  "content-length",
42114
42165
  "transfer-encoding"
42115
42166
  ]) delete outHeaders[drop];
42116
- applyRequestParameters(config.requestParameters, req, {
42117
- headers: outHeaders,
42118
- urlObj: void 0
42119
- });
42167
+ applyRequestParameters(config.requestParameters, req, { headers: outHeaders });
42120
42168
  const fetchImpl = deps.fetch ?? globalThis.fetch;
42121
42169
  const fetchInit = {
42122
42170
  method,
@@ -42184,10 +42232,7 @@ async function dispatchHttpIntegration(config, req, deps) {
42184
42232
  "content-length",
42185
42233
  "transfer-encoding"
42186
42234
  ]) delete outHeaders[drop];
42187
- applyRequestParameters(config.requestParameters, req, {
42188
- headers: outHeaders,
42189
- urlObj: void 0
42190
- });
42235
+ applyRequestParameters(config.requestParameters, req, { headers: outHeaders });
42191
42236
  const fetchImpl = deps.fetch ?? globalThis.fetch;
42192
42237
  const fetchInit = {
42193
42238
  method,
@@ -42282,46 +42327,48 @@ async function dispatchAwsLambdaIntegration(config, req, deps) {
42282
42327
  })
42283
42328
  };
42284
42329
  }
42285
- let invokeOutcome;
42286
42330
  try {
42287
- invokeOutcome = await invokeRie(handle.containerHost, handle.hostPort, eventPayload, deps.rieTimeoutMs);
42288
- } catch (err) {
42289
- deps.pool.release(handle);
42331
+ let invokeOutcome;
42332
+ try {
42333
+ invokeOutcome = await invokeRie(handle.containerHost, handle.hostPort, eventPayload, deps.rieTimeoutMs);
42334
+ } catch (err) {
42335
+ return {
42336
+ statusCode: 502,
42337
+ headers: { "content-type": "application/json" },
42338
+ body: JSON.stringify({
42339
+ message: "AWS Lambda non-proxy invocation failed",
42340
+ reason: err instanceof Error ? err.message : String(err)
42341
+ })
42342
+ };
42343
+ }
42344
+ const payload = invokeOutcome.payload;
42345
+ const isError = payload !== null && typeof payload === "object" && "errorMessage" in payload;
42346
+ const matchTarget = isError ? String(payload["errorMessage"]) : "success";
42347
+ const selected = selectIntegrationResponse(config.responses, matchTarget, isError ? 500 : 200);
42348
+ const respCtx = buildVtlContextFromRequest(req, JSON.stringify(payload ?? null), payload);
42349
+ let body = "";
42350
+ let contentType = "application/json";
42351
+ if (selected.entry) {
42352
+ const picked = pickResponseTemplate(selected.entry.ResponseTemplates, req.headers["accept"]);
42353
+ if (picked) {
42354
+ try {
42355
+ body = evaluateVtl(picked.template, respCtx);
42356
+ } catch (err) {
42357
+ return vtlFailure("response", err, picked.template);
42358
+ }
42359
+ contentType = picked.contentType;
42360
+ } else body = JSON.stringify(payload ?? null);
42361
+ } else body = JSON.stringify(payload ?? null);
42362
+ const headers = { "content-type": contentType };
42363
+ Object.assign(headers, evaluateResponseParameters(selected.entry?.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`AWS Lambda non-proxy response: ${reason}`) }));
42290
42364
  return {
42291
- statusCode: 502,
42292
- headers: { "content-type": "application/json" },
42293
- body: JSON.stringify({
42294
- message: "AWS Lambda non-proxy invocation failed",
42295
- reason: err instanceof Error ? err.message : String(err)
42296
- })
42365
+ statusCode: selected.statusCode,
42366
+ headers,
42367
+ body
42297
42368
  };
42369
+ } finally {
42370
+ deps.pool.release(handle);
42298
42371
  }
42299
- deps.pool.release(handle);
42300
- const payload = invokeOutcome.payload;
42301
- const isError = payload !== null && typeof payload === "object" && "errorMessage" in payload;
42302
- const matchTarget = isError ? String(payload["errorMessage"]) : "success";
42303
- const selected = selectIntegrationResponse(config.responses, matchTarget, isError ? 500 : 200);
42304
- const respCtx = buildVtlContextFromRequest(req, JSON.stringify(payload ?? null), payload);
42305
- let body = "";
42306
- let contentType = "application/json";
42307
- if (selected.entry) {
42308
- const picked = pickResponseTemplate(selected.entry.ResponseTemplates, req.headers["accept"]);
42309
- if (picked) {
42310
- try {
42311
- body = evaluateVtl(picked.template, respCtx);
42312
- } catch (err) {
42313
- return vtlFailure("response", err, picked.template);
42314
- }
42315
- contentType = picked.contentType;
42316
- } else body = JSON.stringify(payload ?? null);
42317
- } else body = JSON.stringify(payload ?? null);
42318
- const headers = { "content-type": contentType };
42319
- Object.assign(headers, evaluateResponseParameters(selected.entry?.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`AWS Lambda non-proxy response: ${reason}`) }));
42320
- return {
42321
- statusCode: selected.statusCode,
42322
- headers,
42323
- body
42324
- };
42325
42372
  }
42326
42373
  function buildVtlContextFromRequest(req, body, inputRoot) {
42327
42374
  return {
@@ -42362,29 +42409,57 @@ function pickRequestTemplate(requestTemplates, contentType) {
42362
42409
  /**
42363
42410
  * Extract `{"statusCode": <N>}` from a rendered MOCK request template.
42364
42411
  * AWS uses this single key to drive `IntegrationResponses[]` selection.
42412
+ *
42413
+ * Returns `undefined` when the rendered template is not JSON OR does not
42414
+ * carry a `{statusCode: N}` object OR the value is not a positive integer
42415
+ * (Issue (#507) item 6 — `Number.isInteger` rejects `"200abc"` /
42416
+ * fractional values that `Number.parseInt` would silently accept). On any
42417
+ * fallback case the caller falls back to the default `IntegrationResponses[]`
42418
+ * entry. The fallback path is logged at debug (Issue (#507) item 3) so
42419
+ * users diagnosing a MOCK dispatch see what AWS would have seen.
42365
42420
  */
42366
42421
  function extractStatusCodeFromRendered(rendered) {
42422
+ const logFallback = (reason) => {
42423
+ const truncated = rendered.length > 200 ? rendered.slice(0, 200) + "..." : rendered;
42424
+ getLogger().child("start-api").debug(`MOCK request template did not yield a statusCode selection driver (${reason}); falling back to the default IntegrationResponses[] entry. Rendered output: ${truncated}`);
42425
+ };
42426
+ let parsed;
42367
42427
  try {
42368
- const parsed = JSON.parse(rendered);
42369
- if (parsed && typeof parsed === "object" && "statusCode" in parsed) {
42370
- const val = parsed["statusCode"];
42371
- if (typeof val === "number") return val;
42372
- if (typeof val === "string") {
42373
- const n = Number.parseInt(val, 10);
42374
- if (Number.isFinite(n)) return n;
42375
- }
42376
- }
42377
- } catch {}
42428
+ parsed = JSON.parse(rendered);
42429
+ } catch {
42430
+ return logFallback("rendered output is not valid JSON");
42431
+ }
42432
+ if (!parsed || typeof parsed !== "object" || !("statusCode" in parsed)) return logFallback("rendered output has no statusCode field");
42433
+ const val = parsed["statusCode"];
42434
+ if (typeof val === "number") {
42435
+ if (Number.isInteger(val) && val >= 100 && val < 600) return val;
42436
+ return logFallback(`statusCode ${val} is out of HTTP range [100, 600)`);
42437
+ }
42438
+ if (typeof val === "string") {
42439
+ const trimmed = val.trim();
42440
+ if (trimmed === "") return logFallback("statusCode is empty / whitespace");
42441
+ const n = Number(trimmed);
42442
+ if (!Number.isInteger(n)) return logFallback(`statusCode '${val}' is not a valid integer`);
42443
+ if (n < 100 || n >= 600) return logFallback(`statusCode ${n} is out of HTTP range [100, 600)`);
42444
+ return n;
42445
+ }
42446
+ return logFallback(`statusCode has unexpected type '${typeof val}'`);
42378
42447
  }
42379
42448
  function defaultResponseEntry(entries) {
42380
42449
  return entries.find((e) => e.SelectionPattern === void 0 || e.SelectionPattern === "") ?? null;
42381
42450
  }
42382
42451
  function parseStatus(raw) {
42383
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
42384
- if (typeof raw === "string") {
42385
- const n = Number.parseInt(raw, 10);
42386
- if (Number.isFinite(n)) return n;
42452
+ if (typeof raw === "number") {
42453
+ if (Number.isInteger(raw) && raw >= 100 && raw < 600) return raw;
42454
+ return;
42387
42455
  }
42456
+ if (typeof raw !== "string") return void 0;
42457
+ const trimmed = raw.trim();
42458
+ if (trimmed === "") return void 0;
42459
+ const n = Number(trimmed);
42460
+ if (!Number.isInteger(n)) return void 0;
42461
+ if (n < 100 || n >= 600) return void 0;
42462
+ return n;
42388
42463
  }
42389
42464
  /**
42390
42465
  * Heuristic: is the given HTTP `Content-Type` header value likely to
@@ -42463,10 +42538,12 @@ function safeJsonParse(s) {
42463
42538
  *
42464
42539
  * Supported key shapes:
42465
42540
  * - `integration.request.header.<name>` → outgoing header
42466
- * - `integration.request.querystring.<name>` → query string param
42467
- * - `integration.request.path.<name>` → path placeholder substitution
42541
+ * - `integration.request.querystring.<name>` → query string param (warn-and-skip)
42542
+ * - `integration.request.path.<name>` → path placeholder substitution (warn-and-skip)
42468
42543
  *
42469
- * Supported value shapes:
42544
+ * Supported value shapes (header case-insensitive + multi-value comma-joined,
42545
+ * querystring case-sensitive + last-wins; see {@link resolveRequestParameterValue}
42546
+ * and `vtl-engine.ts` `$input.params` for the matching read-side semantics):
42470
42547
  * - `method.request.header.<name>` → read incoming header
42471
42548
  * - `method.request.querystring.<name>` → read incoming query param
42472
42549
  * - `method.request.path.<name>` → read path parameter
@@ -42474,6 +42551,12 @@ function safeJsonParse(s) {
42474
42551
  *
42475
42552
  * Unsupported mapping expressions are logged at warn and skipped (matches
42476
42553
  * the ResponseParameters handling in `integration-response-selector.ts`).
42554
+ *
42555
+ * Note: querystring / path-rewrite branches currently warn-and-skip; cdkd
42556
+ * relies on `{paramName}` URI substitution for the canonical case (see
42557
+ * {@link substituteUriPlaceholders}). The previous `urlObj` parameter was
42558
+ * never used by the unimplemented querystring rewrite branch and has been
42559
+ * dropped (Issue (#507) item 2).
42477
42560
  */
42478
42561
  function applyRequestParameters(requestParameters, req, out) {
42479
42562
  if (!requestParameters) return;
@@ -42493,6 +42576,22 @@ function applyRequestParameters(requestParameters, req, out) {
42493
42576
  else logger.warn(`Unsupported RequestParameter key '${key}'; skipping.`);
42494
42577
  }
42495
42578
  }
42579
+ /**
42580
+ * Resolve a single `RequestParameters` value to a string.
42581
+ *
42582
+ * Case-sensitivity contract (Issue (#507) item 5; mirrored on the VTL
42583
+ * read side by `vtl-engine.ts` `$input.params`):
42584
+ *
42585
+ * - Header lookups are **case-insensitive** (the incoming-header map is
42586
+ * pre-lowercased by the http-server) and multi-value duplicates are
42587
+ * comma-joined.
42588
+ * - Querystring lookups are **case-sensitive** (matches AWS API Gateway's
42589
+ * deployed behavior) and multi-value duplicates surface only the
42590
+ * last-wins string (the http-server's request snapshot collapses
42591
+ * duplicates at parse time).
42592
+ * - Path parameters are case-sensitive (CFn template `{paramName}`
42593
+ * placeholders are case-sensitive by construction).
42594
+ */
42496
42595
  function resolveRequestParameterValue(raw, req) {
42497
42596
  if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
42498
42597
  const headerMatch = /^method\.request\.header\.(.+)$/.exec(raw);
@@ -43986,7 +44085,7 @@ function attachAuthorizers(stacks, routes) {
43986
44085
  const out = [];
43987
44086
  const errors = [];
43988
44087
  for (const route of routes) {
43989
- if (route.unsupported || route.mockCors || route.serviceIntegration) {
44088
+ if (route.unsupported || route.mockCors) {
43990
44089
  out.push({ route });
43991
44090
  continue;
43992
44091
  }
@@ -44016,45 +44115,6 @@ function attachAuthorizers(stacks, routes) {
44016
44115
  return out;
44017
44116
  }
44018
44117
  /**
44019
- * Walk every discovered route and return the service-integration routes
44020
- * whose source CFn resource declares an authorizer (HTTP API v2 routes
44021
- * with `AuthorizationType !== 'NONE'`). Service-integration routes only
44022
- * exist on `AWS::ApiGatewayV2::Route`; REST v1 service integrations are
44023
- * a different shape and not yet supported.
44024
- */
44025
- function findIgnoredServiceIntegrationAuthorizers(stacks, routes) {
44026
- const stackByRoute = /* @__PURE__ */ new Map();
44027
- for (const stack of stacks) {
44028
- const prefix = `${stack.stackName}/`;
44029
- for (const route of routes) if (route.declaredAt.startsWith(prefix)) stackByRoute.set(route.declaredAt, stack);
44030
- }
44031
- const out = [];
44032
- for (const route of routes) {
44033
- if (!route.serviceIntegration) continue;
44034
- const stack = stackByRoute.get(route.declaredAt);
44035
- if (!stack) continue;
44036
- const slash = route.declaredAt.indexOf("/");
44037
- if (slash < 0) continue;
44038
- const logicalId = route.declaredAt.slice(slash + 1);
44039
- const resource = stack.template.Resources?.[logicalId];
44040
- if (!resource || resource.Type !== "AWS::ApiGatewayV2::Route") continue;
44041
- const props = resource.Properties ?? {};
44042
- const authType = props["AuthorizationType"];
44043
- if (typeof authType !== "string" || authType === "" || authType.toUpperCase() === "NONE") continue;
44044
- let authorizerName;
44045
- if (authType === "AWS_IAM") authorizerName = "AWS_IAM";
44046
- else {
44047
- const ref = props["AuthorizerId"];
44048
- authorizerName = (ref && typeof ref === "object" && "Ref" in ref && typeof ref.Ref === "string" ? ref.Ref : void 0) ?? `<authType=${authType}, AuthorizerId malformed>`;
44049
- }
44050
- out.push({
44051
- declaredAt: route.declaredAt,
44052
- authorizerName
44053
- });
44054
- }
44055
- return out;
44056
- }
44057
- /**
44058
44118
  * Detect the authorizer (if any) attached to a discovered route.
44059
44119
  * Walks the original CFn resource for the route in `stack.template`.
44060
44120
  */
@@ -45325,6 +45385,11 @@ function resolveSingleReference(ref, ctx) {
45325
45385
  }
45326
45386
  if (ref.startsWith("$context.")) {
45327
45387
  const key = ref.substring(9);
45388
+ if (key === "authorizer" || key.startsWith("authorizer.")) {
45389
+ if (!ctx.authorizer) return "";
45390
+ if (key === "authorizer") return JSON.stringify(ctx.authorizer);
45391
+ return resolveAuthorizerPath(ctx.authorizer, key.substring(11));
45392
+ }
45328
45393
  return ctx.context[key] ?? "";
45329
45394
  }
45330
45395
  if (ref.startsWith("$stageVariables.")) {
@@ -45368,6 +45433,36 @@ function resolveBodyJsonPath(body, path) {
45368
45433
  if (typeof cursor === "string") return cursor;
45369
45434
  return JSON.stringify(cursor);
45370
45435
  }
45436
+ /**
45437
+ * Walk a dot-separated path against the authorizer verdict map (#502).
45438
+ * Used by `$context.authorizer.X` selection expressions on
45439
+ * service-integration routes. Mirrors `resolveBodyJsonPath`'s shape:
45440
+ * - Empty leaf / undefined → `""`.
45441
+ * - String leaf → returned verbatim.
45442
+ * - Non-string leaf → `JSON.stringify`'d (matches AWS-deployed
45443
+ * behavior; `$context.authorizer.jwt.claims` resolves to the JSON
45444
+ * object as a string).
45445
+ *
45446
+ * Array indexing via `[N]` is supported (rare for authorizer
45447
+ * verdicts but cheap to support).
45448
+ */
45449
+ function resolveAuthorizerPath(authorizer, path) {
45450
+ if (path === "") return "";
45451
+ const segments = path.split(/\.|\[(\d+)\]/).filter((s) => s !== void 0 && s !== "");
45452
+ let cursor = authorizer;
45453
+ for (const seg of segments) {
45454
+ if (cursor === null || cursor === void 0) return "";
45455
+ if (typeof cursor !== "object") return "";
45456
+ if (Array.isArray(cursor)) {
45457
+ const idx = Number(seg);
45458
+ if (!Number.isInteger(idx)) return "";
45459
+ cursor = cursor[idx];
45460
+ } else cursor = cursor[seg];
45461
+ }
45462
+ if (cursor === void 0 || cursor === null) return "";
45463
+ if (typeof cursor === "string") return cursor;
45464
+ return JSON.stringify(cursor);
45465
+ }
45371
45466
 
45372
45467
  //#endregion
45373
45468
  //#region src/local/http-server.ts
@@ -45469,10 +45564,6 @@ async function handleRequest(req, res, state, opts) {
45469
45564
  writeNotImplemented(res, match.route.unsupported.reason);
45470
45565
  return;
45471
45566
  }
45472
- if (match.route.serviceIntegration) {
45473
- await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts);
45474
- return;
45475
- }
45476
45567
  const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
45477
45568
  const clientCert = opts.mtls ? extractClientCert(req) : void 0;
45478
45569
  const snapshot = {
@@ -45507,6 +45598,10 @@ async function handleRequest(req, res, state, opts) {
45507
45598
  const overlay = buildOverlay(authorizer, authResult);
45508
45599
  if (overlay) baseEvent = applyAuthorizerOverlay(baseEvent, overlay);
45509
45600
  }
45601
+ if (match.route.serviceIntegration) {
45602
+ await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult);
45603
+ return;
45604
+ }
45510
45605
  if (match.route.restV1Integration) {
45511
45606
  try {
45512
45607
  writeIntegrationOutcome(res, await dispatchRestV1Integration(match.route.restV1Integration, snapshot, matchCtx, state, opts));
@@ -46072,7 +46167,7 @@ function writeError(res, statusCode, body = "{\"message\":\"Internal server erro
46072
46167
  * auth pass. A follow-up PR can hoist the auth pass earlier — keeping it
46073
46168
  * out of this PR limits the blast radius.
46074
46169
  */
46075
- async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46170
+ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult) {
46076
46171
  const route = match.route;
46077
46172
  const svc = route.serviceIntegration;
46078
46173
  if (!svc) {
@@ -46084,6 +46179,7 @@ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46084
46179
  const queryString = parseQueryStringSingular(rawUrl);
46085
46180
  const requestPath = rawUrl.split("?")[0] ?? "/";
46086
46181
  const context = buildServiceIntegrationContextVars(req, route);
46182
+ const authorizerCtx = buildAuthorizerContextForServiceIntegration(authorizer, authResult);
46087
46183
  const ctx = {
46088
46184
  headers: headersFlat,
46089
46185
  queryString,
@@ -46091,7 +46187,8 @@ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46091
46187
  requestPath,
46092
46188
  body: bodyBuf.toString("utf8"),
46093
46189
  context,
46094
- stageVariables: route.stageVariables ?? {}
46190
+ stageVariables: route.stageVariables ?? {},
46191
+ ...authorizerCtx && { authorizer: authorizerCtx }
46095
46192
  };
46096
46193
  const outcome = resolveServiceIntegrationParameters(svc.requestParameters, ctx);
46097
46194
  if (outcome.kind === "error") {
@@ -46162,6 +46259,44 @@ function buildServiceIntegrationContextVars(req, route) {
46162
46259
  };
46163
46260
  }
46164
46261
  /**
46262
+ * Build the `authorizer` field for the parameter-mapping context on
46263
+ * service-integration routes (#502). Surfaces the authorizer's verdict
46264
+ * in the same shape `applyAuthorizerOverlay` writes onto the Lambda
46265
+ * event so users can reference `$context.authorizer.X` /
46266
+ * `$context.authorizer.jwt.claims.X` / `$context.authorizer.claims.X`
46267
+ * in `RequestParameters`.
46268
+ *
46269
+ * Per-kind shape:
46270
+ * - Lambda authorizers (`lambda-token` / `lambda-request` / `iam`):
46271
+ * `principalId` + flat `context` fields land at the top level
46272
+ * (`$context.authorizer.principalId`, `$context.authorizer.<key>`).
46273
+ * - Cognito (REST v1): claims under `$context.authorizer.claims.X`.
46274
+ * - JWT (HTTP v2): claims under `$context.authorizer.jwt.claims.X` +
46275
+ * `$context.authorizer.jwt.scopes`.
46276
+ *
46277
+ * Returns `undefined` when no authorizer fired (route had
46278
+ * `AuthorizationType: NONE` / no authorizer attached).
46279
+ */
46280
+ function buildAuthorizerContextForServiceIntegration(authorizer, result) {
46281
+ if (!authorizer || !result) return void 0;
46282
+ if (authorizer.kind === "lambda-token" || authorizer.kind === "lambda-request") {
46283
+ const ctx = {};
46284
+ if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
46285
+ if (result.context) Object.assign(ctx, result.context);
46286
+ return ctx;
46287
+ }
46288
+ if (authorizer.kind === "iam") {
46289
+ const ctx = {};
46290
+ if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
46291
+ return ctx;
46292
+ }
46293
+ if (authorizer.kind === "cognito") return { claims: { ...result.context ?? {} } };
46294
+ return { jwt: {
46295
+ claims: { ...result.context ?? {} },
46296
+ scopes: []
46297
+ } };
46298
+ }
46299
+ /**
46165
46300
  * Write the 501 Not Implemented response surfaced for routes the
46166
46301
  * discovery layer flagged as `unsupported`. The integration's reason
46167
46302
  * (e.g. "MOCK integration is not emulated", "WebSocket APIs are not
@@ -46923,7 +47058,6 @@ async function localStartApiCommand(target, options) {
46923
47058
  warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
46924
47059
  sigV4CredentialsLoader = defaultCredentialsLoader();
46925
47060
  warnIamRoutes(initialMaterial.routes);
46926
- warnIgnoredServiceIntegrationAuthorizers(initialMaterial.routes, initialMaterial.stacks ?? []);
46927
47061
  let maxTimeoutSec = 0;
46928
47062
  for (const spec of initialMaterial.specs.values()) if (spec.lambda.timeoutSec > maxTimeoutSec) maxTimeoutSec = spec.lambda.timeoutSec;
46929
47063
  const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
@@ -47185,27 +47319,6 @@ function warnIamRoutes(routesWithAuth) {
47185
47319
  return true;
47186
47320
  }
47187
47321
  /**
47188
- * #458 / PR #500 review: emit a one-line warn naming every service-
47189
- * integration route whose source CFn resource declares an authorizer
47190
- * (HTTP API v2 routes with `AuthorizationType !== 'NONE'`). The
47191
- * dispatcher in `http-server.ts` runs the SDK call BEFORE the
47192
- * authorizer pass would fire, so without this warn a CDK app that
47193
- * wires JWT / Lambda / Cognito / IAM authorizers onto service
47194
- * integrations would silently let every local request reach the SDK
47195
- * call without auth. Threading the auth pass through the
47196
- * service-integration dispatcher is a follow-up issue. Returns the
47197
- * number of warned routes so tests can assert the firing path; the
47198
- * value is otherwise unused.
47199
- */
47200
- function warnIgnoredServiceIntegrationAuthorizers(routesWithAuth, stacks) {
47201
- const logger = getLogger();
47202
- const ignored = findIgnoredServiceIntegrationAuthorizers(stacks, routesWithAuth.map((entry) => entry.route));
47203
- if (ignored.length === 0) return 0;
47204
- 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.`);
47205
- for (const entry of ignored) logger.warn(` - ${entry.declaredAt}: authorizer '${entry.authorizerName}' is configured but ignored`);
47206
- return ignored.length;
47207
- }
47208
- /**
47209
47322
  * Build the per-Lambda container spec — code dir, env vars (template +
47210
47323
  * --env-vars overlay), STS-issued creds when --assume-role names this
47211
47324
  * Lambda, optional --debug-port reservation. Errors out with a clear
@@ -51774,7 +51887,7 @@ function reorderArgs(argv) {
51774
51887
  */
51775
51888
  async function main() {
51776
51889
  const program = new Command();
51777
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.132.3");
51890
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.133.0");
51778
51891
  program.addCommand(createBootstrapCommand());
51779
51892
  program.addCommand(createSynthCommand());
51780
51893
  program.addCommand(createListCommand());