@go-to-k/cdkd 0.162.1 → 0.162.3

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
@@ -51328,6 +51328,56 @@ function matchPreflight(req, config) {
51328
51328
  };
51329
51329
  }
51330
51330
  /**
51331
+ * Apply CORS headers to an **actual** (non-preflight) response. CORS
51332
+ * spec requires that 2xx / 4xx / 5xx responses all carry
51333
+ * `Access-Control-Allow-Origin` (only preflight responses also need
51334
+ * `Allow-Methods` / `Allow-Headers` / `Max-Age`) — without it the
51335
+ * browser blocks the response body from JS regardless of status code.
51336
+ *
51337
+ * Looks up the route's `apiLogicalId` in `corsConfigByApiId`. When a
51338
+ * matching config + the request Origin satisfies `AllowOrigins`, sets:
51339
+ *
51340
+ * - `Access-Control-Allow-Origin: <origin or *>` (always)
51341
+ * - `Vary: Origin` (when origin echoed)
51342
+ * - `Access-Control-Allow-Credentials: true` (when configured)
51343
+ * - `Access-Control-Expose-Headers: <list>` (when configured)
51344
+ *
51345
+ * `Allow-Methods` / `Allow-Headers` / `Max-Age` are preflight-only and
51346
+ * deliberately NOT set on actual responses.
51347
+ *
51348
+ * Caller must invoke this BEFORE writing the response body (otherwise
51349
+ * `res.headersSent` flips and `setHeader` becomes a no-op). Idempotent:
51350
+ * a second call with the same args overwrites the same headers.
51351
+ *
51352
+ * No-op when:
51353
+ * - `route.apiLogicalId` is undefined (no surface-config-bearing
51354
+ * resource — e.g. routes discovered before issue #644's apiLogicalId
51355
+ * plumbing existed; harmless on routes without CORS to apply)
51356
+ * - The route's API has no entry in `corsConfigByApiId`
51357
+ * - The request has no `Origin` header (non-CORS request — same
51358
+ * posture as the matchPreflight gate)
51359
+ * - The Origin is not in the AllowOrigins list (browser will block
51360
+ * anyway; we don't smuggle through unauthorized origins by accident)
51361
+ */
51362
+ function applyCorsResponseHeaders(res, apiLogicalId, corsConfigByApiId, requestOrigin) {
51363
+ if (!apiLogicalId) return;
51364
+ const cors = corsConfigByApiId.get(apiLogicalId);
51365
+ if (!cors) return;
51366
+ if (!requestOrigin) return;
51367
+ const originMatch = matchOrigin(requestOrigin, cors.AllowOrigins);
51368
+ if (!originMatch) return;
51369
+ const allowOrigin = originMatch === "*" && cors.AllowCredentials !== true ? "*" : requestOrigin;
51370
+ res.setHeader("Access-Control-Allow-Origin", allowOrigin);
51371
+ if (allowOrigin !== "*") {
51372
+ const existing = res.getHeader("Vary");
51373
+ if (typeof existing === "string" && existing.length > 0) {
51374
+ if (!existing.split(",").map((t) => t.trim()).some((t) => t.toLowerCase() === "origin")) res.setHeader("Vary", `${existing}, Origin`);
51375
+ } else res.setHeader("Vary", "Origin");
51376
+ }
51377
+ if (cors.AllowCredentials === true) res.setHeader("Access-Control-Allow-Credentials", "true");
51378
+ if (cors.ExposeHeaders.length > 0) res.setHeader("Access-Control-Expose-Headers", cors.ExposeHeaders.join(","));
51379
+ }
51380
+ /**
51331
51381
  * Whether the request's Origin matches the AllowOrigins list. Returns
51332
51382
  * `'*'` when a wildcard matched, the literal entry on a literal match,
51333
51383
  * `null` otherwise. The literal case is used by the caller to decide
@@ -53237,11 +53287,16 @@ async function handleRequest(req, res, state, opts) {
53237
53287
  writeError(res, 404, "{\"message\":\"Not Found\"}");
53238
53288
  return;
53239
53289
  }
53290
+ const requestOrigin = pickFirstHeaderValue(collectHeaders(req), "origin") ?? void 0;
53291
+ const applyCors = () => {
53292
+ applyCorsResponseHeaders(res, match.route.apiLogicalId, state.corsConfigByApiId, requestOrigin);
53293
+ };
53240
53294
  if (match.route.mockCors) {
53241
53295
  writeMockCorsPreflight(res, match.route.mockCors);
53242
53296
  return;
53243
53297
  }
53244
53298
  if (match.route.unsupported) {
53299
+ applyCors();
53245
53300
  writeNotImplemented(res, match.route.unsupported.reason);
53246
53301
  return;
53247
53302
  }
@@ -53268,10 +53323,12 @@ async function handleRequest(req, res, state, opts) {
53268
53323
  outcome = await runAuthorizerPass(authorizer, snapshot, matchCtx, state, opts, baseEvent["requestContext"]);
53269
53324
  } catch (err) {
53270
53325
  logger.error(`Authorizer ${authorizer.logicalId} threw for ${match.route.declaredAt}: ${err instanceof Error ? err.message : String(err)}`);
53326
+ applyCors();
53271
53327
  writeAuthRejection(res, match.route.apiVersion, "policy-deny", authorizer.kind);
53272
53328
  return;
53273
53329
  }
53274
53330
  if (!outcome.result.allow) {
53331
+ applyCors();
53275
53332
  writeAuthRejection(res, match.route.apiVersion, outcome.denyKind ?? "policy-deny", authorizer.kind);
53276
53333
  return;
53277
53334
  }
@@ -53280,16 +53337,21 @@ async function handleRequest(req, res, state, opts) {
53280
53337
  if (overlay) baseEvent = applyAuthorizerOverlay(baseEvent, overlay);
53281
53338
  }
53282
53339
  if (match.route.serviceIntegration) {
53340
+ applyCors();
53283
53341
  await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult);
53284
53342
  return;
53285
53343
  }
53286
53344
  if (match.route.restV1Integration) {
53287
53345
  try {
53288
- writeIntegrationOutcome(res, await dispatchRestV1Integration(match.route.restV1Integration, snapshot, matchCtx, state, opts));
53346
+ const outcome = await dispatchRestV1Integration(match.route.restV1Integration, snapshot, matchCtx, state, opts);
53347
+ applyCors();
53348
+ writeIntegrationOutcome(res, outcome);
53289
53349
  } catch (err) {
53290
53350
  logger.error(`REST v1 ${match.route.restV1Integration.kind} dispatch failed for ${match.route.declaredAt}: ${err instanceof Error ? err.message : String(err)}`);
53291
- if (!res.headersSent) writeError(res, 502);
53292
- else res.end();
53351
+ if (!res.headersSent) {
53352
+ applyCors();
53353
+ writeError(res, 502);
53354
+ } else res.end();
53293
53355
  }
53294
53356
  return;
53295
53357
  }
@@ -53298,6 +53360,7 @@ async function handleRequest(req, res, state, opts) {
53298
53360
  handle = await state.pool.acquire(match.route.lambdaLogicalId);
53299
53361
  } catch (err) {
53300
53362
  logger.error(`Failed to acquire container for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
53363
+ applyCors();
53301
53364
  writeError(res, 502);
53302
53365
  return;
53303
53366
  }
@@ -53306,6 +53369,7 @@ async function handleRequest(req, res, state, opts) {
53306
53369
  try {
53307
53370
  streamResult = await invokeRieStreaming(handle.containerHost, handle.hostPort, baseEvent, opts.rieTimeoutMs);
53308
53371
  try {
53372
+ applyCors();
53309
53373
  writeStreamingResponse(res, streamResult, () => state.pool.release(handle));
53310
53374
  } catch (writeErr) {
53311
53375
  streamResult.body.on("error", () => {});
@@ -53315,8 +53379,10 @@ async function handleRequest(req, res, state, opts) {
53315
53379
  return;
53316
53380
  } catch (err) {
53317
53381
  logger.error(`RIE streaming invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
53318
- if (!res.headersSent) writeError(res, 502);
53319
- else res.end();
53382
+ if (!res.headersSent) {
53383
+ applyCors();
53384
+ writeError(res, 502);
53385
+ } else res.end();
53320
53386
  state.pool.release(handle);
53321
53387
  return;
53322
53388
  }
@@ -53326,11 +53392,14 @@ async function handleRequest(req, res, state, opts) {
53326
53392
  res.statusCode = translated.statusCode;
53327
53393
  for (const [name, value] of Object.entries(translated.headers)) res.setHeader(name, value);
53328
53394
  if (translated.cookies.length > 0) res.setHeader("set-cookie", translated.cookies);
53395
+ applyCors();
53329
53396
  res.end(translated.body);
53330
53397
  } catch (err) {
53331
53398
  logger.error(`RIE invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
53332
- if (!res.headersSent) writeError(res, 502);
53333
- else res.end();
53399
+ if (!res.headersSent) {
53400
+ applyCors();
53401
+ writeError(res, 502);
53402
+ } else res.end();
53334
53403
  } finally {
53335
53404
  state.pool.release(handle);
53336
53405
  }
@@ -54668,6 +54737,7 @@ async function localStartApiCommand(target, options) {
54668
54737
  for (const [k, v] of direct) corsConfigByApiId.set(k, v);
54669
54738
  }
54670
54739
  const stateByStack = options.fromState || isCfnFlagPresent(options) ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
54740
+ const profileCredentials = options.profile ? await resolveProfileCredentials(options.profile) : void 0;
54671
54741
  const lambdaIds = uniqueLambdaIds(routes, routesWithAuth, webSocketApis);
54672
54742
  const specs = /* @__PURE__ */ new Map();
54673
54743
  for (let i = 0; i < lambdaIds.length; i++) {
@@ -54684,7 +54754,8 @@ async function localStartApiCommand(target, options) {
54684
54754
  layerTmpDirs,
54685
54755
  stateByStack,
54686
54756
  skipPull: options.pull === false,
54687
- ...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn }
54757
+ ...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn },
54758
+ ...profileCredentials && { profileCredentials }
54688
54759
  });
54689
54760
  specs.set(logicalId, spec);
54690
54761
  }
@@ -55118,7 +55189,7 @@ function warnIamRoutes(routesWithAuth) {
55118
55189
  * missing, runtime not supported).
55119
55190
  */
55120
55191
  async function buildContainerSpec(args) {
55121
- const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn } = args;
55192
+ const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn, profileCredentials } = args;
55122
55193
  const lambda = resolveLambdaByLogicalId(logicalId, stacks);
55123
55194
  let codeDir;
55124
55195
  let optDir;
@@ -55164,7 +55235,15 @@ async function buildContainerSpec(args) {
55164
55235
  dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
55165
55236
  dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
55166
55237
  if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
55167
- } else forwardAwsEnv$1(dockerEnv);
55238
+ } else {
55239
+ forwardAwsEnv$1(dockerEnv);
55240
+ if (profileCredentials) {
55241
+ dockerEnv["AWS_ACCESS_KEY_ID"] = profileCredentials.accessKeyId;
55242
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = profileCredentials.secretAccessKey;
55243
+ if (profileCredentials.sessionToken) dockerEnv["AWS_SESSION_TOKEN"] = profileCredentials.sessionToken;
55244
+ else delete dockerEnv["AWS_SESSION_TOKEN"];
55245
+ }
55246
+ }
55168
55247
  if (debugPort !== void 0) dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
55169
55248
  const tmpfs = lambda.ephemeralStorageMb !== void 0 ? {
55170
55249
  target: "/tmp",
@@ -55527,6 +55606,54 @@ function forwardAwsEnv$1(env) {
55527
55606
  }
55528
55607
  }
55529
55608
  /**
55609
+ * Issue #654: resolve `--profile <p>` to a concrete credential set
55610
+ * for forwarding to Lambda containers.
55611
+ *
55612
+ * The dev's AWS credentials may live in any of:
55613
+ * - `~/.aws/sso/cache/*.json` (AWS IAM Identity Center / legacy SSO)
55614
+ * - `~/.aws/credentials` (regular long-lived access keys)
55615
+ * - `~/.aws/config` profiles with `role_arn` + `source_profile` (chained AssumeRole)
55616
+ * - `credential_process` external resolvers
55617
+ *
55618
+ * `forwardAwsEnv` only reads `process.env.AWS_*`, which is empty for
55619
+ * every shape except "user manually exported the env vars". The
55620
+ * Lambda container therefore boots without creds and the handler's
55621
+ * AWS SDK call fails with `Could not load credentials from any providers`.
55622
+ *
55623
+ * This helper constructs a transient `STSClient({ profile })` to drive
55624
+ * the SDK's default credential provider chain — same code path cdkd's
55625
+ * own CFn / CC API clients use when `--profile` is set, so SSO / IAM
55626
+ * Identity Center / role-assumption profiles all resolve the same way
55627
+ * they already do for cdkd's outbound calls. We then extract the
55628
+ * resolved `AwsCredentialIdentity` via `sts.config.credentials()` and
55629
+ * return the underlying `{ accessKeyId, secretAccessKey, sessionToken? }`
55630
+ * for env-var injection.
55631
+ *
55632
+ * Called ONCE at server boot; the resolved creds are reused for every
55633
+ * Lambda container's env overlay (when `--assume-role` is not set for
55634
+ * that Lambda — assume-role wins per the existing precedence). SSO
55635
+ * temp creds typically last 1h+, so a single resolve is fine for the
55636
+ * common dev session; long-running `--watch` sessions that outlive
55637
+ * the creds need a cdkd restart (deferred refresh out of scope for
55638
+ * v1, see issue #654).
55639
+ */
55640
+ async function resolveProfileCredentials(profile) {
55641
+ const { STSClient } = await import("@aws-sdk/client-sts");
55642
+ const sts = new STSClient({ profile });
55643
+ try {
55644
+ const credsProvider = sts.config.credentials;
55645
+ const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
55646
+ if (!creds || !creds.accessKeyId || !creds.secretAccessKey) throw new Error(`--profile '${profile}': credential provider chain resolved without usable credentials. Check \`aws sso login --profile ` + profile + "` for SSO profiles, or `~/.aws/credentials` / `~/.aws/config` for regular profiles.");
55647
+ return {
55648
+ accessKeyId: creds.accessKeyId,
55649
+ secretAccessKey: creds.secretAccessKey,
55650
+ ...creds.sessionToken && { sessionToken: creds.sessionToken }
55651
+ };
55652
+ } finally {
55653
+ sts.destroy();
55654
+ }
55655
+ }
55656
+ /**
55530
55657
  * Issue an STS AssumeRole and return temporary credentials. Mirrors
55531
55658
  * `cdkd local invoke`'s helper byte-for-byte; lifted here so the
55532
55659
  * start-api command stays self-contained.
@@ -60201,7 +60328,7 @@ function reorderArgs(argv) {
60201
60328
  */
60202
60329
  async function main() {
60203
60330
  const program = new Command();
60204
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.162.1");
60331
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.162.3");
60205
60332
  program.addCommand(createBootstrapCommand());
60206
60333
  program.addCommand(createSynthCommand());
60207
60334
  program.addCommand(createListCommand());