@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 +215 -10
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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)
|
|
53158
|
-
|
|
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)
|
|
53185
|
-
|
|
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)
|
|
53199
|
-
|
|
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
|
|
54532
|
-
for (const [k, v] of
|
|
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.
|
|
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());
|