@go-to-k/cdkd 0.162.0 → 0.162.2

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
@@ -51153,6 +51153,140 @@ function pickStringArray(value) {
51153
51153
  return out;
51154
51154
  }
51155
51155
  /**
51156
+ * Build a `fnUrlLogicalId → CorsConfig` map by tracing CloudFront →
51157
+ * Function URL chains in the template (issue #646).
51158
+ *
51159
+ * Production-correct CDK pattern: Function URL fronted by a CloudFront
51160
+ * Distribution where CORS is declared on the CloudFront
51161
+ * `ResponseHeadersPolicy` (NOT on the Function URL itself). Without this
51162
+ * helper, `cdkd local start-api` sees `Cors: null` on the Function URL
51163
+ * and emits no preflight headers — even though the CDK code correctly
51164
+ * declares the allowed origins on the CloudFront side.
51165
+ *
51166
+ * Detection: an `AWS::CloudFront::Distribution` whose `Origins[].DomainName`
51167
+ * matches the canonical CDK 2.x shape
51168
+ * `Fn::Select[2, Fn::Split['/', Fn::GetAtt[<FnUrlLogicalId>, 'FunctionUrl']]]`
51169
+ * is the chain marker. For each such origin, we walk every cache behavior
51170
+ * (`DefaultCacheBehavior` + `CacheBehaviors[]`), resolve their
51171
+ * `ResponseHeadersPolicyId: { Ref: <RhpLogicalId> }` to the
51172
+ * `AWS::CloudFront::ResponseHeadersPolicy` resource, and extract its
51173
+ * `Properties.ResponseHeadersPolicyConfig.CorsConfig`.
51174
+ *
51175
+ * Schema mapping (CloudFront → internal `CorsConfig`):
51176
+ *
51177
+ * AccessControlAllowOrigins.Items → AllowOrigins
51178
+ * AccessControlAllowMethods.Items → AllowMethods
51179
+ * AccessControlAllowHeaders.Items → AllowHeaders
51180
+ * AccessControlExposeHeaders.Items → ExposeHeaders
51181
+ * AccessControlMaxAgeSec → MaxAge
51182
+ * AccessControlAllowCredentials → AllowCredentials
51183
+ * (OriginOverride is ignored — cdkd has only one config slot)
51184
+ *
51185
+ * Multiple distributions fronting the same Function URL: last write
51186
+ * wins (rare in practice). Per-path CORS via `CacheBehaviors[]` is
51187
+ * NOT supported in v1 — the `DefaultCacheBehavior`'s policy applies
51188
+ * to all paths.
51189
+ */
51190
+ function buildCorsConfigFromCloudFrontChain(template) {
51191
+ const out = /* @__PURE__ */ new Map();
51192
+ const resources = template.Resources ?? {};
51193
+ for (const [, resource] of Object.entries(resources)) {
51194
+ if (resource.Type !== "AWS::CloudFront::Distribution") continue;
51195
+ const distConfig = (resource.Properties ?? {})["DistributionConfig"];
51196
+ if (!distConfig || typeof distConfig !== "object") continue;
51197
+ const dc = distConfig;
51198
+ const origins = Array.isArray(dc["Origins"]) ? dc["Origins"] : [];
51199
+ for (const origin of origins) {
51200
+ if (!origin || typeof origin !== "object") continue;
51201
+ const fnUrlLogicalId = pickFnUrlLogicalIdFromOriginDomainName(origin["DomainName"]);
51202
+ if (!fnUrlLogicalId) continue;
51203
+ const cacheBehaviors = [dc["DefaultCacheBehavior"], ...Array.isArray(dc["CacheBehaviors"]) ? dc["CacheBehaviors"] : []];
51204
+ for (const behavior of cacheBehaviors) {
51205
+ if (!behavior || typeof behavior !== "object") continue;
51206
+ const rhpId = pickRhpRefLogicalId(behavior["ResponseHeadersPolicyId"]);
51207
+ if (!rhpId) continue;
51208
+ const rhpResource = resources[rhpId];
51209
+ if (!rhpResource || rhpResource.Type !== "AWS::CloudFront::ResponseHeadersPolicy") continue;
51210
+ const rhpConfig = (rhpResource.Properties ?? {})["ResponseHeadersPolicyConfig"];
51211
+ if (!rhpConfig || typeof rhpConfig !== "object") continue;
51212
+ const corsConfig = rhpConfig["CorsConfig"];
51213
+ if (!corsConfig || typeof corsConfig !== "object" || Array.isArray(corsConfig)) continue;
51214
+ const parsed = parseCloudFrontCorsConfig(corsConfig);
51215
+ if (parsed) out.set(fnUrlLogicalId, parsed);
51216
+ }
51217
+ }
51218
+ }
51219
+ return out;
51220
+ }
51221
+ /**
51222
+ * Detect the canonical CDK 2.x `DomainName` shape that points a
51223
+ * CloudFront Origin at a Function URL:
51224
+ * {Fn::Select: [2, {Fn::Split: ['/', {Fn::GetAtt: [<id>, 'FunctionUrl']}]}]}
51225
+ * Returns the Function URL's logical ID, or undefined if the shape
51226
+ * doesn't match.
51227
+ */
51228
+ function pickFnUrlLogicalIdFromOriginDomainName(value) {
51229
+ if (!value || typeof value !== "object") return void 0;
51230
+ const sel = value["Fn::Select"];
51231
+ if (!Array.isArray(sel) || sel.length !== 2 || sel[0] !== 2) return void 0;
51232
+ const split = sel[1];
51233
+ if (!split || typeof split !== "object") return void 0;
51234
+ const splitArgs = split["Fn::Split"];
51235
+ if (!Array.isArray(splitArgs) || splitArgs.length !== 2 || splitArgs[0] !== "/") return void 0;
51236
+ const getAtt = splitArgs[1];
51237
+ if (!getAtt || typeof getAtt !== "object") return void 0;
51238
+ const ga = getAtt["Fn::GetAtt"];
51239
+ if (!Array.isArray(ga) || ga.length !== 2 || typeof ga[0] !== "string" || ga[1] !== "FunctionUrl") return;
51240
+ return ga[0];
51241
+ }
51242
+ /**
51243
+ * Unwrap a `ResponseHeadersPolicyId` value to its referenced logical
51244
+ * ID. CDK 2.x synthesizes this as `{ Ref: <id> }`. Returns undefined
51245
+ * for the AWS-managed-policy ID form (literal UUID string) since
51246
+ * cdkd can't fetch those — and for any non-Ref shape.
51247
+ */
51248
+ function pickRhpRefLogicalId(value) {
51249
+ if (!value || typeof value !== "object") return void 0;
51250
+ const ref = value["Ref"];
51251
+ if (typeof ref !== "string" || ref.length === 0) return void 0;
51252
+ return ref;
51253
+ }
51254
+ /**
51255
+ * Parse a CloudFront `ResponseHeadersPolicyConfig.CorsConfig` block
51256
+ * into the internal `CorsConfig` shape. Schema differs from Function
51257
+ * URL / HTTP API v2 (`AccessControl*` prefix + nested `Items` wrapper);
51258
+ * see `buildCorsConfigFromCloudFrontChain` JSDoc for the field mapping.
51259
+ *
51260
+ * Returns undefined when every value-bearing field is missing.
51261
+ */
51262
+ function parseCloudFrontCorsConfig(raw) {
51263
+ const allowOrigins = pickItemsStringArray(raw["AccessControlAllowOrigins"]);
51264
+ const allowMethods = pickItemsStringArray(raw["AccessControlAllowMethods"]);
51265
+ const allowHeaders = pickItemsStringArray(raw["AccessControlAllowHeaders"]);
51266
+ const exposeHeaders = pickItemsStringArray(raw["AccessControlExposeHeaders"]);
51267
+ const maxAgeRaw = raw["AccessControlMaxAgeSec"];
51268
+ const allowCreds = raw["AccessControlAllowCredentials"];
51269
+ if (allowOrigins.length === 0 && allowMethods.length === 0 && allowHeaders.length === 0 && exposeHeaders.length === 0 && maxAgeRaw === void 0 && allowCreds === void 0) return;
51270
+ const config = {
51271
+ AllowOrigins: allowOrigins,
51272
+ AllowMethods: allowMethods,
51273
+ AllowHeaders: allowHeaders,
51274
+ ExposeHeaders: exposeHeaders
51275
+ };
51276
+ if (typeof maxAgeRaw === "number" && Number.isFinite(maxAgeRaw)) config.MaxAge = Math.trunc(maxAgeRaw);
51277
+ if (typeof allowCreds === "boolean") config.AllowCredentials = allowCreds;
51278
+ return config;
51279
+ }
51280
+ /**
51281
+ * CloudFront `AccessControl*Origins/Methods/Headers` use a nested
51282
+ * `Items: string[]` wrapper. Unwrap to a plain `string[]`.
51283
+ */
51284
+ function pickItemsStringArray(value) {
51285
+ if (!value || typeof value !== "object") return [];
51286
+ const items = value["Items"];
51287
+ return pickStringArray(items);
51288
+ }
51289
+ /**
51156
51290
  * Try to match an OPTIONS preflight request against the given CORS
51157
51291
  * config. Returns the canonical response when every check passes;
51158
51292
  * `null` when the request didn't satisfy AllowOrigins / AllowMethods /
@@ -51194,6 +51328,56 @@ function matchPreflight(req, config) {
51194
51328
  };
51195
51329
  }
51196
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
+ /**
51197
51381
  * Whether the request's Origin matches the AllowOrigins list. Returns
51198
51382
  * `'*'` when a wildcard matched, the literal entry on a literal match,
51199
51383
  * `null` otherwise. The literal case is used by the caller to decide
@@ -53103,11 +53287,16 @@ async function handleRequest(req, res, state, opts) {
53103
53287
  writeError(res, 404, "{\"message\":\"Not Found\"}");
53104
53288
  return;
53105
53289
  }
53290
+ const requestOrigin = pickFirstHeaderValue(collectHeaders(req), "origin") ?? void 0;
53291
+ const applyCors = () => {
53292
+ applyCorsResponseHeaders(res, match.route.apiLogicalId, state.corsConfigByApiId, requestOrigin);
53293
+ };
53106
53294
  if (match.route.mockCors) {
53107
53295
  writeMockCorsPreflight(res, match.route.mockCors);
53108
53296
  return;
53109
53297
  }
53110
53298
  if (match.route.unsupported) {
53299
+ applyCors();
53111
53300
  writeNotImplemented(res, match.route.unsupported.reason);
53112
53301
  return;
53113
53302
  }
@@ -53134,10 +53323,12 @@ async function handleRequest(req, res, state, opts) {
53134
53323
  outcome = await runAuthorizerPass(authorizer, snapshot, matchCtx, state, opts, baseEvent["requestContext"]);
53135
53324
  } catch (err) {
53136
53325
  logger.error(`Authorizer ${authorizer.logicalId} threw for ${match.route.declaredAt}: ${err instanceof Error ? err.message : String(err)}`);
53326
+ applyCors();
53137
53327
  writeAuthRejection(res, match.route.apiVersion, "policy-deny", authorizer.kind);
53138
53328
  return;
53139
53329
  }
53140
53330
  if (!outcome.result.allow) {
53331
+ applyCors();
53141
53332
  writeAuthRejection(res, match.route.apiVersion, outcome.denyKind ?? "policy-deny", authorizer.kind);
53142
53333
  return;
53143
53334
  }
@@ -53146,16 +53337,21 @@ async function handleRequest(req, res, state, opts) {
53146
53337
  if (overlay) baseEvent = applyAuthorizerOverlay(baseEvent, overlay);
53147
53338
  }
53148
53339
  if (match.route.serviceIntegration) {
53340
+ applyCors();
53149
53341
  await handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, authorizer, authResult);
53150
53342
  return;
53151
53343
  }
53152
53344
  if (match.route.restV1Integration) {
53153
53345
  try {
53154
- 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);
53155
53349
  } catch (err) {
53156
53350
  logger.error(`REST v1 ${match.route.restV1Integration.kind} dispatch failed for ${match.route.declaredAt}: ${err instanceof Error ? err.message : String(err)}`);
53157
- if (!res.headersSent) writeError(res, 502);
53158
- else res.end();
53351
+ if (!res.headersSent) {
53352
+ applyCors();
53353
+ writeError(res, 502);
53354
+ } else res.end();
53159
53355
  }
53160
53356
  return;
53161
53357
  }
@@ -53164,6 +53360,7 @@ async function handleRequest(req, res, state, opts) {
53164
53360
  handle = await state.pool.acquire(match.route.lambdaLogicalId);
53165
53361
  } catch (err) {
53166
53362
  logger.error(`Failed to acquire container for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
53363
+ applyCors();
53167
53364
  writeError(res, 502);
53168
53365
  return;
53169
53366
  }
@@ -53172,6 +53369,7 @@ async function handleRequest(req, res, state, opts) {
53172
53369
  try {
53173
53370
  streamResult = await invokeRieStreaming(handle.containerHost, handle.hostPort, baseEvent, opts.rieTimeoutMs);
53174
53371
  try {
53372
+ applyCors();
53175
53373
  writeStreamingResponse(res, streamResult, () => state.pool.release(handle));
53176
53374
  } catch (writeErr) {
53177
53375
  streamResult.body.on("error", () => {});
@@ -53181,8 +53379,10 @@ async function handleRequest(req, res, state, opts) {
53181
53379
  return;
53182
53380
  } catch (err) {
53183
53381
  logger.error(`RIE streaming invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
53184
- if (!res.headersSent) writeError(res, 502);
53185
- else res.end();
53382
+ if (!res.headersSent) {
53383
+ applyCors();
53384
+ writeError(res, 502);
53385
+ } else res.end();
53186
53386
  state.pool.release(handle);
53187
53387
  return;
53188
53388
  }
@@ -53192,11 +53392,14 @@ async function handleRequest(req, res, state, opts) {
53192
53392
  res.statusCode = translated.statusCode;
53193
53393
  for (const [name, value] of Object.entries(translated.headers)) res.setHeader(name, value);
53194
53394
  if (translated.cookies.length > 0) res.setHeader("set-cookie", translated.cookies);
53395
+ applyCors();
53195
53396
  res.end(translated.body);
53196
53397
  } catch (err) {
53197
53398
  logger.error(`RIE invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
53198
- if (!res.headersSent) writeError(res, 502);
53199
- else res.end();
53399
+ if (!res.headersSent) {
53400
+ applyCors();
53401
+ writeError(res, 502);
53402
+ } else res.end();
53200
53403
  } finally {
53201
53404
  state.pool.release(handle);
53202
53405
  }
@@ -54528,8 +54731,10 @@ async function localStartApiCommand(target, options) {
54528
54731
  }
54529
54732
  const corsConfigByApiId = /* @__PURE__ */ new Map();
54530
54733
  for (const stack of targetStacks) {
54531
- const m = buildCorsConfigByApiId(stack.template);
54532
- for (const [k, v] of m) corsConfigByApiId.set(k, v);
54734
+ const fromCloudFront = buildCorsConfigFromCloudFrontChain(stack.template);
54735
+ for (const [k, v] of fromCloudFront) corsConfigByApiId.set(k, v);
54736
+ const direct = buildCorsConfigByApiId(stack.template);
54737
+ for (const [k, v] of direct) corsConfigByApiId.set(k, v);
54533
54738
  }
54534
54739
  const stateByStack = options.fromState || isCfnFlagPresent(options) ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
54535
54740
  const lambdaIds = uniqueLambdaIds(routes, routesWithAuth, webSocketApis);
@@ -60065,7 +60270,7 @@ function reorderArgs(argv) {
60065
60270
  */
60066
60271
  async function main() {
60067
60272
  const program = new Command();
60068
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.162.0");
60273
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.162.2");
60069
60274
  program.addCommand(createBootstrapCommand());
60070
60275
  program.addCommand(createSynthCommand());
60071
60276
  program.addCommand(createListCommand());