@go-to-k/cdkd 0.132.4 → 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
@@ -44085,7 +44085,7 @@ function attachAuthorizers(stacks, routes) {
44085
44085
  const out = [];
44086
44086
  const errors = [];
44087
44087
  for (const route of routes) {
44088
- if (route.unsupported || route.mockCors || route.serviceIntegration) {
44088
+ if (route.unsupported || route.mockCors) {
44089
44089
  out.push({ route });
44090
44090
  continue;
44091
44091
  }
@@ -44115,45 +44115,6 @@ function attachAuthorizers(stacks, routes) {
44115
44115
  return out;
44116
44116
  }
44117
44117
  /**
44118
- * Walk every discovered route and return the service-integration routes
44119
- * whose source CFn resource declares an authorizer (HTTP API v2 routes
44120
- * with `AuthorizationType !== 'NONE'`). Service-integration routes only
44121
- * exist on `AWS::ApiGatewayV2::Route`; REST v1 service integrations are
44122
- * a different shape and not yet supported.
44123
- */
44124
- function findIgnoredServiceIntegrationAuthorizers(stacks, routes) {
44125
- const stackByRoute = /* @__PURE__ */ new Map();
44126
- for (const stack of stacks) {
44127
- const prefix = `${stack.stackName}/`;
44128
- for (const route of routes) if (route.declaredAt.startsWith(prefix)) stackByRoute.set(route.declaredAt, stack);
44129
- }
44130
- const out = [];
44131
- for (const route of routes) {
44132
- if (!route.serviceIntegration) continue;
44133
- const stack = stackByRoute.get(route.declaredAt);
44134
- if (!stack) continue;
44135
- const slash = route.declaredAt.indexOf("/");
44136
- if (slash < 0) continue;
44137
- const logicalId = route.declaredAt.slice(slash + 1);
44138
- const resource = stack.template.Resources?.[logicalId];
44139
- if (!resource || resource.Type !== "AWS::ApiGatewayV2::Route") continue;
44140
- const props = resource.Properties ?? {};
44141
- const authType = props["AuthorizationType"];
44142
- if (typeof authType !== "string" || authType === "" || authType.toUpperCase() === "NONE") continue;
44143
- let authorizerName;
44144
- if (authType === "AWS_IAM") authorizerName = "AWS_IAM";
44145
- else {
44146
- const ref = props["AuthorizerId"];
44147
- authorizerName = (ref && typeof ref === "object" && "Ref" in ref && typeof ref.Ref === "string" ? ref.Ref : void 0) ?? `<authType=${authType}, AuthorizerId malformed>`;
44148
- }
44149
- out.push({
44150
- declaredAt: route.declaredAt,
44151
- authorizerName
44152
- });
44153
- }
44154
- return out;
44155
- }
44156
- /**
44157
44118
  * Detect the authorizer (if any) attached to a discovered route.
44158
44119
  * Walks the original CFn resource for the route in `stack.template`.
44159
44120
  */
@@ -45424,6 +45385,11 @@ function resolveSingleReference(ref, ctx) {
45424
45385
  }
45425
45386
  if (ref.startsWith("$context.")) {
45426
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
+ }
45427
45393
  return ctx.context[key] ?? "";
45428
45394
  }
45429
45395
  if (ref.startsWith("$stageVariables.")) {
@@ -45467,6 +45433,36 @@ function resolveBodyJsonPath(body, path) {
45467
45433
  if (typeof cursor === "string") return cursor;
45468
45434
  return JSON.stringify(cursor);
45469
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
+ }
45470
45466
 
45471
45467
  //#endregion
45472
45468
  //#region src/local/http-server.ts
@@ -45568,10 +45564,6 @@ async function handleRequest(req, res, state, opts) {
45568
45564
  writeNotImplemented(res, match.route.unsupported.reason);
45569
45565
  return;
45570
45566
  }
45571
- if (match.route.serviceIntegration) {
45572
- await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts);
45573
- return;
45574
- }
45575
45567
  const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
45576
45568
  const clientCert = opts.mtls ? extractClientCert(req) : void 0;
45577
45569
  const snapshot = {
@@ -45606,6 +45598,10 @@ async function handleRequest(req, res, state, opts) {
45606
45598
  const overlay = buildOverlay(authorizer, authResult);
45607
45599
  if (overlay) baseEvent = applyAuthorizerOverlay(baseEvent, overlay);
45608
45600
  }
45601
+ if (match.route.serviceIntegration) {
45602
+ await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult);
45603
+ return;
45604
+ }
45609
45605
  if (match.route.restV1Integration) {
45610
45606
  try {
45611
45607
  writeIntegrationOutcome(res, await dispatchRestV1Integration(match.route.restV1Integration, snapshot, matchCtx, state, opts));
@@ -46171,7 +46167,7 @@ function writeError(res, statusCode, body = "{\"message\":\"Internal server erro
46171
46167
  * auth pass. A follow-up PR can hoist the auth pass earlier — keeping it
46172
46168
  * out of this PR limits the blast radius.
46173
46169
  */
46174
- async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46170
+ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult) {
46175
46171
  const route = match.route;
46176
46172
  const svc = route.serviceIntegration;
46177
46173
  if (!svc) {
@@ -46183,6 +46179,7 @@ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46183
46179
  const queryString = parseQueryStringSingular(rawUrl);
46184
46180
  const requestPath = rawUrl.split("?")[0] ?? "/";
46185
46181
  const context = buildServiceIntegrationContextVars(req, route);
46182
+ const authorizerCtx = buildAuthorizerContextForServiceIntegration(authorizer, authResult);
46186
46183
  const ctx = {
46187
46184
  headers: headersFlat,
46188
46185
  queryString,
@@ -46190,7 +46187,8 @@ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46190
46187
  requestPath,
46191
46188
  body: bodyBuf.toString("utf8"),
46192
46189
  context,
46193
- stageVariables: route.stageVariables ?? {}
46190
+ stageVariables: route.stageVariables ?? {},
46191
+ ...authorizerCtx && { authorizer: authorizerCtx }
46194
46192
  };
46195
46193
  const outcome = resolveServiceIntegrationParameters(svc.requestParameters, ctx);
46196
46194
  if (outcome.kind === "error") {
@@ -46261,6 +46259,44 @@ function buildServiceIntegrationContextVars(req, route) {
46261
46259
  };
46262
46260
  }
46263
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
+ /**
46264
46300
  * Write the 501 Not Implemented response surfaced for routes the
46265
46301
  * discovery layer flagged as `unsupported`. The integration's reason
46266
46302
  * (e.g. "MOCK integration is not emulated", "WebSocket APIs are not
@@ -47022,7 +47058,6 @@ async function localStartApiCommand(target, options) {
47022
47058
  warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
47023
47059
  sigV4CredentialsLoader = defaultCredentialsLoader();
47024
47060
  warnIamRoutes(initialMaterial.routes);
47025
- warnIgnoredServiceIntegrationAuthorizers(initialMaterial.routes, initialMaterial.stacks ?? []);
47026
47061
  let maxTimeoutSec = 0;
47027
47062
  for (const spec of initialMaterial.specs.values()) if (spec.lambda.timeoutSec > maxTimeoutSec) maxTimeoutSec = spec.lambda.timeoutSec;
47028
47063
  const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
@@ -47284,27 +47319,6 @@ function warnIamRoutes(routesWithAuth) {
47284
47319
  return true;
47285
47320
  }
47286
47321
  /**
47287
- * #458 / PR #500 review: emit a one-line warn naming every service-
47288
- * integration route whose source CFn resource declares an authorizer
47289
- * (HTTP API v2 routes with `AuthorizationType !== 'NONE'`). The
47290
- * dispatcher in `http-server.ts` runs the SDK call BEFORE the
47291
- * authorizer pass would fire, so without this warn a CDK app that
47292
- * wires JWT / Lambda / Cognito / IAM authorizers onto service
47293
- * integrations would silently let every local request reach the SDK
47294
- * call without auth. Threading the auth pass through the
47295
- * service-integration dispatcher is a follow-up issue. Returns the
47296
- * number of warned routes so tests can assert the firing path; the
47297
- * value is otherwise unused.
47298
- */
47299
- function warnIgnoredServiceIntegrationAuthorizers(routesWithAuth, stacks) {
47300
- const logger = getLogger();
47301
- const ignored = findIgnoredServiceIntegrationAuthorizers(stacks, routesWithAuth.map((entry) => entry.route));
47302
- if (ignored.length === 0) return 0;
47303
- 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.`);
47304
- for (const entry of ignored) logger.warn(` - ${entry.declaredAt}: authorizer '${entry.authorizerName}' is configured but ignored`);
47305
- return ignored.length;
47306
- }
47307
- /**
47308
47322
  * Build the per-Lambda container spec — code dir, env vars (template +
47309
47323
  * --env-vars overlay), STS-issued creds when --assume-role names this
47310
47324
  * Lambda, optional --debug-port reservation. Errors out with a clear
@@ -51873,7 +51887,7 @@ function reorderArgs(argv) {
51873
51887
  */
51874
51888
  async function main() {
51875
51889
  const program = new Command();
51876
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.132.4");
51890
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.133.0");
51877
51891
  program.addCommand(createBootstrapCommand());
51878
51892
  program.addCommand(createSynthCommand());
51879
51893
  program.addCommand(createListCommand());