@go-to-k/cdkd 0.132.4 → 0.134.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
@@ -34750,6 +34750,31 @@ function createStateCommand() {
34750
34750
  return cmd;
34751
34751
  }
34752
34752
 
34753
+ //#endregion
34754
+ //#region src/cli/cfn-stack-states.ts
34755
+ /**
34756
+ * Stable terminal CloudFormation stack states.
34757
+ *
34758
+ * Sourced from the AWS CloudFormation API documentation: a stack in any
34759
+ * of these states has settled (success or rolled-back) and is safe to
34760
+ * read / mutate. Every other status (`*_IN_PROGRESS`, `*_FAILED`,
34761
+ * `REVIEW_IN_PROGRESS`) means the stack is mid-operation or in an
34762
+ * unhealthy state — callers gate AWS-side mutations behind this set so
34763
+ * the user can settle the source before paying for further work.
34764
+ *
34765
+ * Single source of truth — consumed by both `cdkd migrate`'s pre-flight
34766
+ * check (`src/cli/commands/migrate/cfn-stack-prefetch.ts`) and the
34767
+ * `cdkd import --migrate-from-cloudformation` retirement flow
34768
+ * (`src/cli/commands/retire-cfn-stack.ts`).
34769
+ */
34770
+ const STABLE_TERMINAL_STATUSES = new Set([
34771
+ "CREATE_COMPLETE",
34772
+ "UPDATE_COMPLETE",
34773
+ "UPDATE_ROLLBACK_COMPLETE",
34774
+ "IMPORT_COMPLETE",
34775
+ "IMPORT_ROLLBACK_COMPLETE"
34776
+ ]);
34777
+
34753
34778
  //#endregion
34754
34779
  //#region src/cli/upload-cfn-template.ts
34755
34780
  /**
@@ -35191,18 +35216,6 @@ function parseCfnTemplateWithFormat(text) {
35191
35216
  //#endregion
35192
35217
  //#region src/cli/commands/retire-cfn-stack.ts
35193
35218
  /**
35194
- * Stack states from which an UpdateStack call is safe. Anything else (an
35195
- * IN_PROGRESS, FAILED, or REVIEW_IN_PROGRESS state) means the stack is
35196
- * mid-operation or in an unhealthy state we should not touch.
35197
- */
35198
- const STABLE_TERMINAL_STATUSES = new Set([
35199
- "CREATE_COMPLETE",
35200
- "UPDATE_COMPLETE",
35201
- "UPDATE_ROLLBACK_COMPLETE",
35202
- "IMPORT_COMPLETE",
35203
- "IMPORT_ROLLBACK_COMPLETE"
35204
- ]);
35205
- /**
35206
35219
  * UpdateStack TemplateBody hard limit (51,200 bytes). Templates larger than
35207
35220
  * this are uploaded to cdkd's state S3 bucket and submitted via `TemplateURL`
35208
35221
  * instead — see {@link uploadTemplateForUpdateStack}.
@@ -44085,7 +44098,7 @@ function attachAuthorizers(stacks, routes) {
44085
44098
  const out = [];
44086
44099
  const errors = [];
44087
44100
  for (const route of routes) {
44088
- if (route.unsupported || route.mockCors || route.serviceIntegration) {
44101
+ if (route.unsupported || route.mockCors) {
44089
44102
  out.push({ route });
44090
44103
  continue;
44091
44104
  }
@@ -44115,45 +44128,6 @@ function attachAuthorizers(stacks, routes) {
44115
44128
  return out;
44116
44129
  }
44117
44130
  /**
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
44131
  * Detect the authorizer (if any) attached to a discovered route.
44158
44132
  * Walks the original CFn resource for the route in `stack.template`.
44159
44133
  */
@@ -45424,6 +45398,11 @@ function resolveSingleReference(ref, ctx) {
45424
45398
  }
45425
45399
  if (ref.startsWith("$context.")) {
45426
45400
  const key = ref.substring(9);
45401
+ if (key === "authorizer" || key.startsWith("authorizer.")) {
45402
+ if (!ctx.authorizer) return "";
45403
+ if (key === "authorizer") return JSON.stringify(ctx.authorizer);
45404
+ return resolveAuthorizerPath(ctx.authorizer, key.substring(11));
45405
+ }
45427
45406
  return ctx.context[key] ?? "";
45428
45407
  }
45429
45408
  if (ref.startsWith("$stageVariables.")) {
@@ -45467,6 +45446,36 @@ function resolveBodyJsonPath(body, path) {
45467
45446
  if (typeof cursor === "string") return cursor;
45468
45447
  return JSON.stringify(cursor);
45469
45448
  }
45449
+ /**
45450
+ * Walk a dot-separated path against the authorizer verdict map (#502).
45451
+ * Used by `$context.authorizer.X` selection expressions on
45452
+ * service-integration routes. Mirrors `resolveBodyJsonPath`'s shape:
45453
+ * - Empty leaf / undefined → `""`.
45454
+ * - String leaf → returned verbatim.
45455
+ * - Non-string leaf → `JSON.stringify`'d (matches AWS-deployed
45456
+ * behavior; `$context.authorizer.jwt.claims` resolves to the JSON
45457
+ * object as a string).
45458
+ *
45459
+ * Array indexing via `[N]` is supported (rare for authorizer
45460
+ * verdicts but cheap to support).
45461
+ */
45462
+ function resolveAuthorizerPath(authorizer, path) {
45463
+ if (path === "") return "";
45464
+ const segments = path.split(/\.|\[(\d+)\]/).filter((s) => s !== void 0 && s !== "");
45465
+ let cursor = authorizer;
45466
+ for (const seg of segments) {
45467
+ if (cursor === null || cursor === void 0) return "";
45468
+ if (typeof cursor !== "object") return "";
45469
+ if (Array.isArray(cursor)) {
45470
+ const idx = Number(seg);
45471
+ if (!Number.isInteger(idx)) return "";
45472
+ cursor = cursor[idx];
45473
+ } else cursor = cursor[seg];
45474
+ }
45475
+ if (cursor === void 0 || cursor === null) return "";
45476
+ if (typeof cursor === "string") return cursor;
45477
+ return JSON.stringify(cursor);
45478
+ }
45470
45479
 
45471
45480
  //#endregion
45472
45481
  //#region src/local/http-server.ts
@@ -45568,10 +45577,6 @@ async function handleRequest(req, res, state, opts) {
45568
45577
  writeNotImplemented(res, match.route.unsupported.reason);
45569
45578
  return;
45570
45579
  }
45571
- if (match.route.serviceIntegration) {
45572
- await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts);
45573
- return;
45574
- }
45575
45580
  const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
45576
45581
  const clientCert = opts.mtls ? extractClientCert(req) : void 0;
45577
45582
  const snapshot = {
@@ -45606,6 +45611,10 @@ async function handleRequest(req, res, state, opts) {
45606
45611
  const overlay = buildOverlay(authorizer, authResult);
45607
45612
  if (overlay) baseEvent = applyAuthorizerOverlay(baseEvent, overlay);
45608
45613
  }
45614
+ if (match.route.serviceIntegration) {
45615
+ await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult);
45616
+ return;
45617
+ }
45609
45618
  if (match.route.restV1Integration) {
45610
45619
  try {
45611
45620
  writeIntegrationOutcome(res, await dispatchRestV1Integration(match.route.restV1Integration, snapshot, matchCtx, state, opts));
@@ -46171,7 +46180,7 @@ function writeError(res, statusCode, body = "{\"message\":\"Internal server erro
46171
46180
  * auth pass. A follow-up PR can hoist the auth pass earlier — keeping it
46172
46181
  * out of this PR limits the blast radius.
46173
46182
  */
46174
- async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46183
+ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult) {
46175
46184
  const route = match.route;
46176
46185
  const svc = route.serviceIntegration;
46177
46186
  if (!svc) {
@@ -46183,6 +46192,7 @@ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46183
46192
  const queryString = parseQueryStringSingular(rawUrl);
46184
46193
  const requestPath = rawUrl.split("?")[0] ?? "/";
46185
46194
  const context = buildServiceIntegrationContextVars(req, route);
46195
+ const authorizerCtx = buildAuthorizerContextForServiceIntegration(authorizer, authResult);
46186
46196
  const ctx = {
46187
46197
  headers: headersFlat,
46188
46198
  queryString,
@@ -46190,7 +46200,8 @@ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts) {
46190
46200
  requestPath,
46191
46201
  body: bodyBuf.toString("utf8"),
46192
46202
  context,
46193
- stageVariables: route.stageVariables ?? {}
46203
+ stageVariables: route.stageVariables ?? {},
46204
+ ...authorizerCtx && { authorizer: authorizerCtx }
46194
46205
  };
46195
46206
  const outcome = resolveServiceIntegrationParameters(svc.requestParameters, ctx);
46196
46207
  if (outcome.kind === "error") {
@@ -46261,6 +46272,44 @@ function buildServiceIntegrationContextVars(req, route) {
46261
46272
  };
46262
46273
  }
46263
46274
  /**
46275
+ * Build the `authorizer` field for the parameter-mapping context on
46276
+ * service-integration routes (#502). Surfaces the authorizer's verdict
46277
+ * in the same shape `applyAuthorizerOverlay` writes onto the Lambda
46278
+ * event so users can reference `$context.authorizer.X` /
46279
+ * `$context.authorizer.jwt.claims.X` / `$context.authorizer.claims.X`
46280
+ * in `RequestParameters`.
46281
+ *
46282
+ * Per-kind shape:
46283
+ * - Lambda authorizers (`lambda-token` / `lambda-request` / `iam`):
46284
+ * `principalId` + flat `context` fields land at the top level
46285
+ * (`$context.authorizer.principalId`, `$context.authorizer.<key>`).
46286
+ * - Cognito (REST v1): claims under `$context.authorizer.claims.X`.
46287
+ * - JWT (HTTP v2): claims under `$context.authorizer.jwt.claims.X` +
46288
+ * `$context.authorizer.jwt.scopes`.
46289
+ *
46290
+ * Returns `undefined` when no authorizer fired (route had
46291
+ * `AuthorizationType: NONE` / no authorizer attached).
46292
+ */
46293
+ function buildAuthorizerContextForServiceIntegration(authorizer, result) {
46294
+ if (!authorizer || !result) return void 0;
46295
+ if (authorizer.kind === "lambda-token" || authorizer.kind === "lambda-request") {
46296
+ const ctx = {};
46297
+ if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
46298
+ if (result.context) Object.assign(ctx, result.context);
46299
+ return ctx;
46300
+ }
46301
+ if (authorizer.kind === "iam") {
46302
+ const ctx = {};
46303
+ if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
46304
+ return ctx;
46305
+ }
46306
+ if (authorizer.kind === "cognito") return { claims: { ...result.context ?? {} } };
46307
+ return { jwt: {
46308
+ claims: { ...result.context ?? {} },
46309
+ scopes: []
46310
+ } };
46311
+ }
46312
+ /**
46264
46313
  * Write the 501 Not Implemented response surfaced for routes the
46265
46314
  * discovery layer flagged as `unsupported`. The integration's reason
46266
46315
  * (e.g. "MOCK integration is not emulated", "WebSocket APIs are not
@@ -47022,7 +47071,6 @@ async function localStartApiCommand(target, options) {
47022
47071
  warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
47023
47072
  sigV4CredentialsLoader = defaultCredentialsLoader();
47024
47073
  warnIamRoutes(initialMaterial.routes);
47025
- warnIgnoredServiceIntegrationAuthorizers(initialMaterial.routes, initialMaterial.stacks ?? []);
47026
47074
  let maxTimeoutSec = 0;
47027
47075
  for (const spec of initialMaterial.specs.values()) if (spec.lambda.timeoutSec > maxTimeoutSec) maxTimeoutSec = spec.lambda.timeoutSec;
47028
47076
  const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
@@ -47284,27 +47332,6 @@ function warnIamRoutes(routesWithAuth) {
47284
47332
  return true;
47285
47333
  }
47286
47334
  /**
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
47335
  * Build the per-Lambda container spec — code dir, env vars (template +
47309
47336
  * --env-vars overlay), STS-issued creds when --assume-role names this
47310
47337
  * Lambda, optional --debug-port reservation. Errors out with a clear
@@ -51873,7 +51900,7 @@ function reorderArgs(argv) {
51873
51900
  */
51874
51901
  async function main() {
51875
51902
  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");
51903
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.134.0");
51877
51904
  program.addCommand(createBootstrapCommand());
51878
51905
  program.addCommand(createSynthCommand());
51879
51906
  program.addCommand(createListCommand());