@go-to-k/cdkd 0.114.1 → 0.115.1
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/README.md +12 -2
- package/dist/cli.js +320 -60
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -459,8 +459,18 @@ One server per discovered API — authorizers, CORS configs, and stage
|
|
|
459
459
|
variables stay scoped to the owning API. Supports REST v1 + HTTP API +
|
|
460
460
|
Function URL with AWS_PROXY integrations; Lambda TOKEN / REQUEST,
|
|
461
461
|
Cognito User Pool, and HTTP v2 JWT authorizers (JWKS-verified); CORS
|
|
462
|
-
preflight
|
|
463
|
-
|
|
462
|
+
preflight (HTTP API v2 `CorsConfiguration` + REST v1 OPTIONS MOCK
|
|
463
|
+
preflight from `defaultCorsPreflightOptions`); hot reload via `--watch`;
|
|
464
|
+
deploy-state-backed env var substitution via `--from-state`.
|
|
465
|
+
|
|
466
|
+
Routes whose integration cdkd cannot emulate (non-AWS_PROXY REST v1
|
|
467
|
+
types other than the MOCK CORS preflight subset, HTTP API v2 service
|
|
468
|
+
integrations, WebSocket APIs, Function URLs with IAM auth or
|
|
469
|
+
RESPONSE_STREAM, cross-stack Lambda Arn references) **do not block
|
|
470
|
+
boot** — the server starts with a per-route `[warn]` summary and
|
|
471
|
+
returns HTTP 501 + the reason in the JSON body if and when the route is
|
|
472
|
+
hit. This lets you run the rest of your API surface locally while the
|
|
473
|
+
unsupported routes stay on the deployed API.
|
|
464
474
|
|
|
465
475
|
### `local run-task`
|
|
466
476
|
|
package/dist/cli.js
CHANGED
|
@@ -38192,7 +38192,7 @@ const INVOKE_ARN_MARKER = ":lambda:path/2015-03-31/functions/";
|
|
|
38192
38192
|
* names the surface-level problem (the caller's location prefix +
|
|
38193
38193
|
* `shortJson` rendering is layered on top). Never throws.
|
|
38194
38194
|
*/
|
|
38195
|
-
function resolveLambdaArnIntrinsic
|
|
38195
|
+
function resolveLambdaArnIntrinsic(value) {
|
|
38196
38196
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {
|
|
38197
38197
|
kind: "unsupported",
|
|
38198
38198
|
detail: "expected an object intrinsic"
|
|
@@ -38319,8 +38319,16 @@ function resolveFnSubInvokeArn(arg) {
|
|
|
38319
38319
|
* lambdaLogicalId, stage) tuple is identical — different stacks may
|
|
38320
38320
|
* legitimately host different APIs that mount the same path.
|
|
38321
38321
|
*
|
|
38322
|
-
*
|
|
38323
|
-
*
|
|
38322
|
+
* Each route is one of three classes (see {@link DiscoveredRoute}):
|
|
38323
|
+
* - normal (no flag set);
|
|
38324
|
+
* - synthetic CORS preflight (`mockCors` set);
|
|
38325
|
+
* - deferred-error unsupported (`unsupported` set).
|
|
38326
|
+
*
|
|
38327
|
+
* Throws {@link RouteDiscoveryError} only on template-structural failures
|
|
38328
|
+
* the discovery layer cannot generate a meaningful route from (e.g.
|
|
38329
|
+
* missing Integration property, ParentId cycle, non-Ref RestApiId). Per-
|
|
38330
|
+
* route integration unsupportedness now flows through `unsupported` and
|
|
38331
|
+
* is surfaced as HTTP 501 at request time.
|
|
38324
38332
|
*/
|
|
38325
38333
|
function discoverRoutes(stacks) {
|
|
38326
38334
|
const routes = [];
|
|
@@ -38345,7 +38353,7 @@ function discoverRoutes(stacks) {
|
|
|
38345
38353
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
38346
38354
|
}
|
|
38347
38355
|
}
|
|
38348
|
-
if (errors.length > 0) throw new RouteDiscoveryError(`cdkd local start-api: ${errors.length}
|
|
38356
|
+
if (errors.length > 0) throw new RouteDiscoveryError(`cdkd local start-api: ${errors.length} malformed route(s) in the synthesized template:\n` + errors.map((e) => ` - ${e}`).join("\n"));
|
|
38349
38357
|
return routes;
|
|
38350
38358
|
}
|
|
38351
38359
|
/**
|
|
@@ -38355,8 +38363,18 @@ function discoverRoutes(stacks) {
|
|
|
38355
38363
|
* the full path, then looks up the corresponding Stage (when one is
|
|
38356
38364
|
* attached to the same RestApi) so `requestContext.stage` is realistic.
|
|
38357
38365
|
*
|
|
38358
|
-
*
|
|
38359
|
-
*
|
|
38366
|
+
* Per-integration classification (see {@link DiscoveredRoute}):
|
|
38367
|
+
* - `Integration.Type === 'AWS_PROXY'` → normal route.
|
|
38368
|
+
* - `HttpMethod === 'OPTIONS'` + `Type === 'MOCK'` + `IntegrationResponses`
|
|
38369
|
+
* contain literal `method.response.header.*` mapping params → synthetic
|
|
38370
|
+
* CORS preflight (`mockCors` set). Emulates CDK's
|
|
38371
|
+
* `defaultCorsPreflightOptions` output.
|
|
38372
|
+
* - All other `Integration.Type` values (`MOCK` without CORS shape,
|
|
38373
|
+
* `AWS`, `HTTP`, `HTTP_PROXY`) → unsupported route. The HTTP server
|
|
38374
|
+
* returns 501 when the route is hit; boot proceeds.
|
|
38375
|
+
*
|
|
38376
|
+
* Hard-errors on template-structural problems (missing Integration,
|
|
38377
|
+
* non-Ref RestApiId, ParentId-chain failures).
|
|
38360
38378
|
*
|
|
38361
38379
|
* Method.HttpMethod values of `'ANY'` are returned as a single route with
|
|
38362
38380
|
* `method='ANY'`; the matcher routes any HTTP method to the Lambda.
|
|
@@ -38365,10 +38383,6 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
|
|
|
38365
38383
|
const props = resource.Properties ?? {};
|
|
38366
38384
|
const integration = props["Integration"];
|
|
38367
38385
|
if (!integration) throw new Error(`${stackName}/${logicalId} (AWS::ApiGateway::Method): missing Integration property`);
|
|
38368
|
-
const integrationType = integration["Type"];
|
|
38369
|
-
if (integrationType !== "AWS_PROXY") throw new Error(`${stackName}/${logicalId} (AWS::ApiGateway::Method): integration type '${String(integrationType)}' is not supported (only AWS_PROXY). MOCK / AWS / HTTP / HTTP_PROXY require mapping templates that cdkd cannot emulate.`);
|
|
38370
|
-
const integrationUri = integration["Uri"];
|
|
38371
|
-
const lambdaLogicalId = resolveLambdaArnIntrinsic(integrationUri, `${stackName}/${logicalId}.Integration.Uri`);
|
|
38372
38386
|
const restApiId = props["RestApiId"];
|
|
38373
38387
|
const restApiLogicalId = pickRefLogicalId$2(restApiId);
|
|
38374
38388
|
if (!restApiLogicalId) throw new Error(`${stackName}/${logicalId} (AWS::ApiGateway::Method): RestApiId must be a { Ref: '...' } reference (got ${shortJson$1(restApiId)}).`);
|
|
@@ -38377,10 +38391,7 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
|
|
|
38377
38391
|
const httpMethod = stringifyValue(props["HttpMethod"] ?? "ANY");
|
|
38378
38392
|
const stage = pickRestV1Stage(restApiLogicalId, template);
|
|
38379
38393
|
const restApiCdkPath = readApiCdkPath(restApiLogicalId, template);
|
|
38380
|
-
|
|
38381
|
-
method: httpMethod,
|
|
38382
|
-
pathPattern: path,
|
|
38383
|
-
lambdaLogicalId,
|
|
38394
|
+
const baseRoute = {
|
|
38384
38395
|
source: "rest-v1",
|
|
38385
38396
|
apiVersion: "v1",
|
|
38386
38397
|
stage,
|
|
@@ -38388,9 +38399,100 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
|
|
|
38388
38399
|
apiStackName: stackName,
|
|
38389
38400
|
...restApiCdkPath !== void 0 && { apiCdkPath: restApiCdkPath },
|
|
38390
38401
|
declaredAt: `${stackName}/${logicalId}`
|
|
38402
|
+
};
|
|
38403
|
+
const integrationType = integration["Type"];
|
|
38404
|
+
if (integrationType === "MOCK") {
|
|
38405
|
+
const preflight = httpMethod === "OPTIONS" ? extractRestV1MockCorsConfig(integration) : void 0;
|
|
38406
|
+
if (preflight) return [{
|
|
38407
|
+
...baseRoute,
|
|
38408
|
+
method: "OPTIONS",
|
|
38409
|
+
pathPattern: path,
|
|
38410
|
+
lambdaLogicalId: "",
|
|
38411
|
+
mockCors: preflight
|
|
38412
|
+
}];
|
|
38413
|
+
return [{
|
|
38414
|
+
...baseRoute,
|
|
38415
|
+
method: httpMethod,
|
|
38416
|
+
pathPattern: path,
|
|
38417
|
+
lambdaLogicalId: "",
|
|
38418
|
+
unsupported: { reason: `${stackName}/${logicalId}: MOCK integration is not emulated (only the CORS preflight subset, where HttpMethod=OPTIONS and IntegrationResponses carry literal method.response.header.* values, is supported).` }
|
|
38419
|
+
}];
|
|
38420
|
+
}
|
|
38421
|
+
if (integrationType !== "AWS_PROXY") return [{
|
|
38422
|
+
...baseRoute,
|
|
38423
|
+
method: httpMethod,
|
|
38424
|
+
pathPattern: path,
|
|
38425
|
+
lambdaLogicalId: "",
|
|
38426
|
+
unsupported: { reason: `${stackName}/${logicalId}: REST v1 integration type '${String(integrationType)}' is not supported (only AWS_PROXY and the MOCK CORS preflight subset).` }
|
|
38427
|
+
}];
|
|
38428
|
+
const integrationUri = integration["Uri"];
|
|
38429
|
+
const arnOutcome = resolveLambdaArnOutcome(integrationUri);
|
|
38430
|
+
if (arnOutcome.kind === "unsupported") return [{
|
|
38431
|
+
...baseRoute,
|
|
38432
|
+
method: httpMethod,
|
|
38433
|
+
pathPattern: path,
|
|
38434
|
+
lambdaLogicalId: "",
|
|
38435
|
+
unsupported: { reason: `${stackName}/${logicalId}.Integration.Uri: ${arnOutcome.detail} (got ${shortJson$1(integrationUri)}). Lambda Arn intrinsics on cross-stack / imported references are not resolvable locally; deploy the producer stack and use \`cdkd local invoke --from-state\` shapes if you need it.` }
|
|
38436
|
+
}];
|
|
38437
|
+
return [{
|
|
38438
|
+
...baseRoute,
|
|
38439
|
+
method: httpMethod,
|
|
38440
|
+
pathPattern: path,
|
|
38441
|
+
lambdaLogicalId: arnOutcome.logicalId
|
|
38391
38442
|
}];
|
|
38392
38443
|
}
|
|
38393
38444
|
/**
|
|
38445
|
+
* Extract the canonical CORS-preflight headers from a REST v1 MOCK
|
|
38446
|
+
* Method's `Integration.IntegrationResponses[0]`. Returns `undefined`
|
|
38447
|
+
* when the shape isn't a CORS preflight (no IntegrationResponses, no
|
|
38448
|
+
* `method.response.header.*` mapping parameters, or any individual
|
|
38449
|
+
* mapping parameter we could not evaluate locally — see below).
|
|
38450
|
+
*
|
|
38451
|
+
* AWS represents header literals in `ResponseParameters` with surrounding
|
|
38452
|
+
* single-quotes (e.g. `"'*'"` for `*`). The single-quote wrappers are
|
|
38453
|
+
* stripped to produce the canonical header value the local server emits.
|
|
38454
|
+
*
|
|
38455
|
+
* **All-or-nothing**: if any `method.response.header.*` entry is
|
|
38456
|
+
* intrinsic-valued (`Fn::Sub`, `Ref` etc.), unquoted, or otherwise
|
|
38457
|
+
* not a string-literal-with-quotes, the WHOLE preflight falls through
|
|
38458
|
+
* to the unsupported class. Emitting a partial preflight with some
|
|
38459
|
+
* headers missing would silently break CORS in the browser (the
|
|
38460
|
+
* preflight succeeds, then the actual request hits a CORS error the
|
|
38461
|
+
* user has to debug through Network panel) — caller's the better
|
|
38462
|
+
* place to surface the underlying VTL-requirement via the 501 path.
|
|
38463
|
+
*
|
|
38464
|
+
* Only the first `IntegrationResponses` entry is consulted. CDK's
|
|
38465
|
+
* `defaultCorsPreflightOptions` emits exactly one entry; hand-rolled
|
|
38466
|
+
* multi-status MOCK preflights are an unsupported v1 limitation.
|
|
38467
|
+
*/
|
|
38468
|
+
function extractRestV1MockCorsConfig(integration) {
|
|
38469
|
+
const responses = integration["IntegrationResponses"];
|
|
38470
|
+
if (!Array.isArray(responses) || responses.length === 0) return void 0;
|
|
38471
|
+
const first = responses[0];
|
|
38472
|
+
if (!first || typeof first !== "object") return void 0;
|
|
38473
|
+
const entry = first;
|
|
38474
|
+
const responseParameters = entry["ResponseParameters"];
|
|
38475
|
+
if (!responseParameters || typeof responseParameters !== "object" || Array.isArray(responseParameters)) return;
|
|
38476
|
+
const headers = {};
|
|
38477
|
+
let sawAnyHeader = false;
|
|
38478
|
+
for (const [key, raw] of Object.entries(responseParameters)) {
|
|
38479
|
+
const m = /^method\.response\.header\.(.+)$/.exec(key);
|
|
38480
|
+
if (!m) continue;
|
|
38481
|
+
sawAnyHeader = true;
|
|
38482
|
+
const headerName = m[1];
|
|
38483
|
+
if (typeof raw !== "string") return void 0;
|
|
38484
|
+
if (raw.length < 2 || raw[0] !== "'" || raw[raw.length - 1] !== "'") return void 0;
|
|
38485
|
+
headers[headerName] = raw.slice(1, -1);
|
|
38486
|
+
}
|
|
38487
|
+
if (!sawAnyHeader) return void 0;
|
|
38488
|
+
const statusCodeRaw = entry["StatusCode"];
|
|
38489
|
+
const parsed = typeof statusCodeRaw === "string" ? Number.parseInt(statusCodeRaw, 10) : NaN;
|
|
38490
|
+
return {
|
|
38491
|
+
statusCode: Number.isFinite(parsed) ? parsed : 204,
|
|
38492
|
+
headers
|
|
38493
|
+
};
|
|
38494
|
+
}
|
|
38495
|
+
/**
|
|
38394
38496
|
* Walk a chain of `AWS::ApiGateway::Resource` parent pointers up to the
|
|
38395
38497
|
* `RestApi` root to build the full path. Each `Resource` contributes a
|
|
38396
38498
|
* `PathPart` segment; the `RestApi` itself contributes the leading `/`.
|
|
@@ -38462,27 +38564,29 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
|
|
|
38462
38564
|
const apiId = props["ApiId"];
|
|
38463
38565
|
const apiLogicalId = pickRefLogicalId$2(apiId);
|
|
38464
38566
|
if (!apiLogicalId) throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): ApiId must be { Ref: '...' } (got ${shortJson$1(apiId)}).`);
|
|
38567
|
+
const routeKey = props["RouteKey"];
|
|
38568
|
+
if (typeof routeKey !== "string" || routeKey.length === 0) throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): RouteKey must be a string`);
|
|
38569
|
+
const apiCdkPath = readApiCdkPath(apiLogicalId, template);
|
|
38465
38570
|
const apiResource = template.Resources?.[apiLogicalId];
|
|
38466
38571
|
if (apiResource?.Type === "AWS::ApiGatewayV2::Api") {
|
|
38467
|
-
if ((apiResource.Properties ?? {})["ProtocolType"] === "WEBSOCKET")
|
|
38572
|
+
if ((apiResource.Properties ?? {})["ProtocolType"] === "WEBSOCKET") return [{
|
|
38573
|
+
method: "ANY",
|
|
38574
|
+
pathPattern: routeKey,
|
|
38575
|
+
lambdaLogicalId: "",
|
|
38576
|
+
source: "http-api",
|
|
38577
|
+
apiVersion: "v2",
|
|
38578
|
+
stage: "$default",
|
|
38579
|
+
apiLogicalId,
|
|
38580
|
+
apiStackName: stackName,
|
|
38581
|
+
...apiCdkPath !== void 0 && { apiCdkPath },
|
|
38582
|
+
declaredAt: `${stackName}/${logicalId}`,
|
|
38583
|
+
unsupported: { reason: `${stackName}/${logicalId}: WebSocket APIs are not supported in cdkd local start-api.` }
|
|
38584
|
+
}];
|
|
38468
38585
|
}
|
|
38469
|
-
const routeKey = props["RouteKey"];
|
|
38470
|
-
if (typeof routeKey !== "string" || routeKey.length === 0) throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): RouteKey must be a string`);
|
|
38471
|
-
const target = props["Target"];
|
|
38472
|
-
const integrationLogicalId = parseHttpApiTargetIntegration(target, `${stackName}/${logicalId}.Target`);
|
|
38473
|
-
const integration = template.Resources?.[integrationLogicalId];
|
|
38474
|
-
if (!integration || integration.Type !== "AWS::ApiGatewayV2::Integration") throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): Target points at '${integrationLogicalId}' which is not an AWS::ApiGatewayV2::Integration`);
|
|
38475
|
-
const integrationProps = integration.Properties ?? {};
|
|
38476
|
-
const integrationType = integrationProps["IntegrationType"];
|
|
38477
|
-
if (integrationType !== "AWS_PROXY") throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): integration type '${String(integrationType)}' is not supported (only AWS_PROXY).`);
|
|
38478
|
-
if (integrationProps["IntegrationSubtype"] !== void 0) throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): IntegrationSubtype '${stringifyValue(integrationProps["IntegrationSubtype"])}' is not supported (ApiGatewayV2 service integrations like SQS/EventBridge cannot run locally).`);
|
|
38479
|
-
const lambdaLogicalId = resolveLambdaArnIntrinsic(integrationProps["IntegrationUri"], `${stackName}/${integrationLogicalId}.IntegrationUri`);
|
|
38480
38586
|
const { method, pathPattern } = parseRouteKey(routeKey);
|
|
38481
|
-
const
|
|
38482
|
-
return [{
|
|
38587
|
+
const baseRoute = {
|
|
38483
38588
|
method,
|
|
38484
38589
|
pathPattern,
|
|
38485
|
-
lambdaLogicalId,
|
|
38486
38590
|
source: "http-api",
|
|
38487
38591
|
apiVersion: "v2",
|
|
38488
38592
|
stage: "$default",
|
|
@@ -38490,35 +38594,82 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
|
|
|
38490
38594
|
apiStackName: stackName,
|
|
38491
38595
|
...apiCdkPath !== void 0 && { apiCdkPath },
|
|
38492
38596
|
declaredAt: `${stackName}/${logicalId}`
|
|
38597
|
+
};
|
|
38598
|
+
const target = props["Target"];
|
|
38599
|
+
const integrationLogicalId = parseHttpApiTargetIntegration(target, `${stackName}/${logicalId}.Target`);
|
|
38600
|
+
const integration = template.Resources?.[integrationLogicalId];
|
|
38601
|
+
if (!integration || integration.Type !== "AWS::ApiGatewayV2::Integration") throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): Target points at '${integrationLogicalId}' which is not an AWS::ApiGatewayV2::Integration`);
|
|
38602
|
+
const integrationProps = integration.Properties ?? {};
|
|
38603
|
+
const integrationType = integrationProps["IntegrationType"];
|
|
38604
|
+
if (integrationType !== "AWS_PROXY") return [{
|
|
38605
|
+
...baseRoute,
|
|
38606
|
+
lambdaLogicalId: "",
|
|
38607
|
+
unsupported: { reason: `${stackName}/${logicalId}: HTTP API v2 integration type '${String(integrationType)}' is not supported (only AWS_PROXY).` }
|
|
38608
|
+
}];
|
|
38609
|
+
if (integrationProps["IntegrationSubtype"] !== void 0) return [{
|
|
38610
|
+
...baseRoute,
|
|
38611
|
+
lambdaLogicalId: "",
|
|
38612
|
+
unsupported: { reason: `${stackName}/${logicalId}: HTTP API v2 service integration with IntegrationSubtype '${stringifyValue(integrationProps["IntegrationSubtype"])}' is not supported (cdkd cannot proxy directly to SQS / EventBridge / etc.).` }
|
|
38613
|
+
}];
|
|
38614
|
+
const arnOutcome = resolveLambdaArnOutcome(integrationProps["IntegrationUri"]);
|
|
38615
|
+
if (arnOutcome.kind === "unsupported") return [{
|
|
38616
|
+
...baseRoute,
|
|
38617
|
+
lambdaLogicalId: "",
|
|
38618
|
+
unsupported: { reason: `${stackName}/${integrationLogicalId}.IntegrationUri: ${arnOutcome.detail} (got ${shortJson$1(integrationProps["IntegrationUri"])}). Lambda Arn intrinsics on cross-stack / imported references are not resolvable locally.` }
|
|
38619
|
+
}];
|
|
38620
|
+
return [{
|
|
38621
|
+
...baseRoute,
|
|
38622
|
+
lambdaLogicalId: arnOutcome.logicalId
|
|
38493
38623
|
}];
|
|
38494
38624
|
}
|
|
38495
38625
|
/**
|
|
38496
38626
|
* Discover the synthetic `ANY /{proxy+}` route from an
|
|
38497
38627
|
* `AWS::Lambda::Url` resource.
|
|
38498
38628
|
*
|
|
38499
|
-
*
|
|
38500
|
-
*
|
|
38501
|
-
*
|
|
38502
|
-
*
|
|
38629
|
+
* Per-shape classification:
|
|
38630
|
+
* - `AuthType === 'NONE'` + `InvokeMode !== 'RESPONSE_STREAM'` → normal route.
|
|
38631
|
+
* - `AuthType !== 'NONE'` (e.g. `AWS_IAM`) → deferred-error
|
|
38632
|
+
* unsupported. Boot proceeds; HTTP 501 + `reason` at request time.
|
|
38633
|
+
* IAM auth would need SigV4 verification cdkd cannot emulate.
|
|
38634
|
+
* - `InvokeMode === 'RESPONSE_STREAM'` → deferred-error unsupported.
|
|
38635
|
+
* The RIE container does not implement `InvokeWithResponseStream`.
|
|
38636
|
+
*
|
|
38637
|
+
* The Lambda Arn intrinsic resolution still **hard-errors** when it
|
|
38638
|
+
* cannot pin down a same-template Lambda — Function URLs have no other
|
|
38639
|
+
* identifying info (no RouteKey / RestApi parent), so the route would
|
|
38640
|
+
* be uninformative as a deferred-501 entry.
|
|
38503
38641
|
*/
|
|
38504
38642
|
function discoverFunctionUrl(logicalId, resource, template, stackName) {
|
|
38505
38643
|
const props = resource.Properties ?? {};
|
|
38506
|
-
const authType = props["AuthType"];
|
|
38507
|
-
if (authType !== "NONE") throw new Error(`${stackName}/${logicalId} (AWS::Lambda::Url): AuthType '${String(authType)}' is not supported (only NONE — IAM auth requires SigV4 verification cdkd cannot emulate locally; deferred follow-up PR).`);
|
|
38508
|
-
if (props["InvokeMode"] === "RESPONSE_STREAM") throw new Error(`${stackName}/${logicalId} (AWS::Lambda::Url): InvokeMode RESPONSE_STREAM is not supported (deferred follow-up PR).`);
|
|
38509
38644
|
const targetArn = props["TargetFunctionArn"];
|
|
38510
|
-
const
|
|
38645
|
+
const arnOutcome = resolveLambdaArnOutcome(targetArn);
|
|
38646
|
+
if (arnOutcome.kind === "unsupported") throw new Error(`${stackName}/${logicalId}.TargetFunctionArn: ${arnOutcome.detail} (got ${shortJson$1(targetArn)}).`);
|
|
38647
|
+
const lambdaLogicalId = arnOutcome.logicalId;
|
|
38511
38648
|
const lambdaCdkPath = readApiCdkPath(lambdaLogicalId, template);
|
|
38512
|
-
|
|
38649
|
+
const baseRoute = {
|
|
38513
38650
|
method: "ANY",
|
|
38514
38651
|
pathPattern: "/{proxy+}",
|
|
38515
|
-
lambdaLogicalId,
|
|
38516
38652
|
source: "function-url",
|
|
38517
38653
|
apiVersion: "v2",
|
|
38518
38654
|
stage: "$default",
|
|
38519
38655
|
apiStackName: stackName,
|
|
38520
38656
|
...lambdaCdkPath !== void 0 && { apiCdkPath: lambdaCdkPath },
|
|
38521
38657
|
declaredAt: `${stackName}/${logicalId}`
|
|
38658
|
+
};
|
|
38659
|
+
const authType = props["AuthType"];
|
|
38660
|
+
if (authType !== "NONE") return [{
|
|
38661
|
+
...baseRoute,
|
|
38662
|
+
lambdaLogicalId,
|
|
38663
|
+
unsupported: { reason: `${stackName}/${logicalId}: AuthType '${String(authType)}' is not supported (only NONE — IAM auth requires SigV4 verification cdkd cannot emulate locally).` }
|
|
38664
|
+
}];
|
|
38665
|
+
if (props["InvokeMode"] === "RESPONSE_STREAM") return [{
|
|
38666
|
+
...baseRoute,
|
|
38667
|
+
lambdaLogicalId,
|
|
38668
|
+
unsupported: { reason: `${stackName}/${logicalId}: InvokeMode RESPONSE_STREAM is not supported (cdkd's RIE container does not implement InvokeWithResponseStream).` }
|
|
38669
|
+
}];
|
|
38670
|
+
return [{
|
|
38671
|
+
...baseRoute,
|
|
38672
|
+
lambdaLogicalId
|
|
38522
38673
|
}];
|
|
38523
38674
|
}
|
|
38524
38675
|
/**
|
|
@@ -38541,13 +38692,11 @@ function readApiCdkPath(logicalId, template) {
|
|
|
38541
38692
|
* invoke-ARN `Fn::Join` wrapper / the `Fn::Sub` invoke-ARN wrapper (both
|
|
38542
38693
|
* 1-arg and 2-arg forms).
|
|
38543
38694
|
*
|
|
38544
|
-
*
|
|
38545
|
-
*
|
|
38546
|
-
*
|
|
38547
|
-
*
|
|
38548
|
-
*
|
|
38549
|
-
* so the top-level catch in `discoverRoutes` re-wraps it as
|
|
38550
|
-
* `RouteDiscoveryError` along with sibling errors.
|
|
38695
|
+
* Non-throwing: returns the shared resolver's discriminated union
|
|
38696
|
+
* unchanged so each call site can decide whether to surface the
|
|
38697
|
+
* unsupported case as a per-route `unsupported` flag (the new default)
|
|
38698
|
+
* or as a hard error (Function URLs, which lack route-level identity
|
|
38699
|
+
* without their Lambda).
|
|
38551
38700
|
*
|
|
38552
38701
|
* **Why we don't reuse `src/deployment/intrinsic-function-resolver.ts`**:
|
|
38553
38702
|
* that resolver is deploy-state-coupled — it pulls in STS / EC2 / Secrets
|
|
@@ -38555,10 +38704,8 @@ function readApiCdkPath(logicalId, template) {
|
|
|
38555
38704
|
* `cdkd local start-api` runs purely against the synthesized template
|
|
38556
38705
|
* and doesn't have any of that.
|
|
38557
38706
|
*/
|
|
38558
|
-
function
|
|
38559
|
-
|
|
38560
|
-
if (outcome.kind === "resolved") return outcome.logicalId;
|
|
38561
|
-
throw new Error(`${location}: ${outcome.detail} (got ${shortJson$1(value)}). Only { Ref: <LambdaLogicalId> }, { 'Fn::GetAtt': [<LambdaLogicalId>, 'Arn'] }, the REST v1 invoke-ARN Fn::Join wrapper, and the Fn::Sub invoke-ARN wrapper are supported. Other intrinsics (Fn::Sub against arbitrary templates, etc.) require deploy-state and are not supported in cdkd local start-api.`);
|
|
38707
|
+
function resolveLambdaArnOutcome(value) {
|
|
38708
|
+
return resolveLambdaArnIntrinsic(value);
|
|
38562
38709
|
}
|
|
38563
38710
|
/**
|
|
38564
38711
|
* Parse an HTTP API Route's `Target` into the integration's logical ID.
|
|
@@ -39840,19 +39987,52 @@ function resolveHttpApiAuthorizer(authorizerLogicalId, routeAuthorizationScopes,
|
|
|
39840
39987
|
throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: AWS::ApiGatewayV2::Authorizer.AuthorizerType '${String(authType)}' is not supported by cdkd local start-api (only REQUEST / JWT).`);
|
|
39841
39988
|
}
|
|
39842
39989
|
/**
|
|
39990
|
+
* Thrown by {@link resolveLambdaArn} when the authorizer's
|
|
39991
|
+
* `AuthorizerUri` intrinsic does not resolve to a same-template Lambda
|
|
39992
|
+
* (cross-stack reference, imported Lambda, hand-rolled `Fn::Sub` outside
|
|
39993
|
+
* the invoke-ARN wrapper).
|
|
39994
|
+
*
|
|
39995
|
+
* Caught by {@link attachAuthorizers} and converted into a per-route
|
|
39996
|
+
* `unsupported` flag — symmetric with how `route-discovery.ts` handles
|
|
39997
|
+
* an unresolvable `IntegrationUri`. The route appears in the route
|
|
39998
|
+
* table as `[501 Not Implemented]` and returns HTTP 501 + the
|
|
39999
|
+
* `reason` at request time. The alternative ("attach no authorizer,
|
|
40000
|
+
* leave route normal") would be **unsafe** — it would let a request
|
|
40001
|
+
* hit a user-protected route without any auth check just because the
|
|
40002
|
+
* authorizer Lambda lives in another stack.
|
|
40003
|
+
*
|
|
40004
|
+
* Private to this module: `attachAuthorizers` is the only legitimate
|
|
40005
|
+
* consumer.
|
|
40006
|
+
*/
|
|
40007
|
+
var AuthorizerLambdaUnresolvableError = class AuthorizerLambdaUnresolvableError extends RouteDiscoveryError {
|
|
40008
|
+
reason;
|
|
40009
|
+
constructor(reason) {
|
|
40010
|
+
super(reason);
|
|
40011
|
+
this.reason = reason;
|
|
40012
|
+
this.name = "AuthorizerLambdaUnresolvableError";
|
|
40013
|
+
Object.setPrototypeOf(this, AuthorizerLambdaUnresolvableError.prototype);
|
|
40014
|
+
}
|
|
40015
|
+
};
|
|
40016
|
+
/**
|
|
39843
40017
|
* Resolve a Lambda ARN intrinsic to its logical ID. Delegates to the
|
|
39844
40018
|
* shared `resolveLambdaArnIntrinsic` in `intrinsic-lambda-arn.ts`
|
|
39845
40019
|
* (extracted in issue #286 Gaps 3 / 4); accepts `Ref` /
|
|
39846
40020
|
* `Fn::GetAtt: [..., 'Arn']` / the REST v1 invoke-ARN `Fn::Join` wrapper
|
|
39847
40021
|
* (now also used by CDK 2.x's `HttpLambdaAuthorizer` for HTTP API v2 —
|
|
39848
40022
|
* verified via real `cdk synth` 2026-05-12) / the `Fn::Sub` invoke-ARN
|
|
39849
|
-
* wrapper (both 1-arg and 2-arg forms).
|
|
39850
|
-
*
|
|
40023
|
+
* wrapper (both 1-arg and 2-arg forms).
|
|
40024
|
+
*
|
|
40025
|
+
* On an unresolvable intrinsic throws {@link AuthorizerLambdaUnresolvableError}
|
|
40026
|
+
* (caught by `attachAuthorizers` and converted into a per-route
|
|
40027
|
+
* deferred-501) instead of the generic `RouteDiscoveryError`, so
|
|
40028
|
+
* `cdkd local start-api` can boot against an app with a cross-stack
|
|
40029
|
+
* authorizer Lambda — symmetric with the route-level `IntegrationUri`
|
|
40030
|
+
* unresolvable case (issue #431).
|
|
39851
40031
|
*/
|
|
39852
40032
|
function resolveLambdaArn(value, location) {
|
|
39853
|
-
const outcome = resolveLambdaArnIntrinsic
|
|
40033
|
+
const outcome = resolveLambdaArnIntrinsic(value);
|
|
39854
40034
|
if (outcome.kind === "resolved") return outcome.logicalId;
|
|
39855
|
-
throw new
|
|
40035
|
+
throw new AuthorizerLambdaUnresolvableError(`${location}: ${outcome.detail} (got ${shortJson(value)}). Only { Ref }, { Fn::GetAtt: [..., 'Arn'] }, the REST v1 invoke-ARN Fn::Join wrapper, and the Fn::Sub invoke-ARN wrapper are supported.`);
|
|
39856
40036
|
}
|
|
39857
40037
|
/**
|
|
39858
40038
|
* REST v1 IdentitySource for TOKEN authorizers must be exactly one
|
|
@@ -40024,6 +40204,10 @@ function attachAuthorizers(stacks, routes) {
|
|
|
40024
40204
|
const out = [];
|
|
40025
40205
|
const errors = [];
|
|
40026
40206
|
for (const route of routes) {
|
|
40207
|
+
if (route.unsupported || route.mockCors) {
|
|
40208
|
+
out.push({ route });
|
|
40209
|
+
continue;
|
|
40210
|
+
}
|
|
40027
40211
|
const stack = stackByRoute.get(route.declaredAt);
|
|
40028
40212
|
if (!stack) {
|
|
40029
40213
|
out.push({ route });
|
|
@@ -40036,6 +40220,13 @@ function attachAuthorizers(stacks, routes) {
|
|
|
40036
40220
|
...authorizer && { authorizer }
|
|
40037
40221
|
});
|
|
40038
40222
|
} catch (err) {
|
|
40223
|
+
if (err instanceof AuthorizerLambdaUnresolvableError) {
|
|
40224
|
+
out.push({ route: {
|
|
40225
|
+
...route,
|
|
40226
|
+
unsupported: { reason: `${route.declaredAt}: authorizer Lambda Arn unresolvable — ${err.reason}` }
|
|
40227
|
+
} });
|
|
40228
|
+
continue;
|
|
40229
|
+
}
|
|
40039
40230
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
40040
40231
|
}
|
|
40041
40232
|
}
|
|
@@ -40778,6 +40969,14 @@ async function handleRequest(req, res, state, opts) {
|
|
|
40778
40969
|
writeError(res, 404, "{\"message\":\"Not Found\"}");
|
|
40779
40970
|
return;
|
|
40780
40971
|
}
|
|
40972
|
+
if (match.route.mockCors) {
|
|
40973
|
+
writeMockCorsPreflight(res, match.route.mockCors);
|
|
40974
|
+
return;
|
|
40975
|
+
}
|
|
40976
|
+
if (match.route.unsupported) {
|
|
40977
|
+
writeNotImplemented(res, match.route.unsupported.reason);
|
|
40978
|
+
return;
|
|
40979
|
+
}
|
|
40781
40980
|
const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
|
|
40782
40981
|
const snapshot = {
|
|
40783
40982
|
method,
|
|
@@ -41191,6 +41390,35 @@ function writeError(res, statusCode, body = "{\"message\":\"Internal server erro
|
|
|
41191
41390
|
res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
|
|
41192
41391
|
res.end(body);
|
|
41193
41392
|
}
|
|
41393
|
+
/**
|
|
41394
|
+
* Write the 501 Not Implemented response surfaced for routes the
|
|
41395
|
+
* discovery layer flagged as `unsupported`. The integration's reason
|
|
41396
|
+
* (e.g. "MOCK integration is not emulated", "WebSocket APIs are not
|
|
41397
|
+
* supported") is echoed in the body so the user gets a precise pointer
|
|
41398
|
+
* at first hit instead of a generic 502.
|
|
41399
|
+
*/
|
|
41400
|
+
function writeNotImplemented(res, reason) {
|
|
41401
|
+
const body = JSON.stringify({
|
|
41402
|
+
message: "Not Implemented",
|
|
41403
|
+
reason
|
|
41404
|
+
});
|
|
41405
|
+
res.statusCode = 501;
|
|
41406
|
+
res.setHeader("content-type", "application/json");
|
|
41407
|
+
res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
|
|
41408
|
+
res.end(body);
|
|
41409
|
+
}
|
|
41410
|
+
/**
|
|
41411
|
+
* Write the canonical CORS preflight response derived from a REST v1
|
|
41412
|
+
* MOCK Method's `Integration.IntegrationResponses[0].ResponseParameters`.
|
|
41413
|
+
* Headers are emitted verbatim — the discovery layer already stripped
|
|
41414
|
+
* AWS's literal single-quote wrappers and dropped any non-literal
|
|
41415
|
+
* (intrinsic-valued) entries.
|
|
41416
|
+
*/
|
|
41417
|
+
function writeMockCorsPreflight(res, preflight) {
|
|
41418
|
+
res.statusCode = preflight.statusCode;
|
|
41419
|
+
for (const [name, value] of Object.entries(preflight.headers)) res.setHeader(name, value);
|
|
41420
|
+
res.end();
|
|
41421
|
+
}
|
|
41194
41422
|
|
|
41195
41423
|
//#endregion
|
|
41196
41424
|
//#region src/local/api-server-grouping.ts
|
|
@@ -41817,6 +42045,7 @@ async function localStartApiCommand(target, options) {
|
|
|
41817
42045
|
if (basePort !== 0) nextPort += 1;
|
|
41818
42046
|
}
|
|
41819
42047
|
printPerServerRouteTables(servers);
|
|
42048
|
+
warnUnsupportedRoutes(servers.flatMap((s) => s.group.routes.map((r) => r.route)), logger);
|
|
41820
42049
|
logger.info(`Per-Lambda concurrency: ${perLambdaConcurrency} (override with --per-lambda-concurrency)`);
|
|
41821
42050
|
for (const { group, server } of servers) process.stdout.write(`Server listening on http://${server.host}:${server.port} (${group.displayName})\n`);
|
|
41822
42051
|
process.stdout.write("^C to stop and clean up containers.\n");
|
|
@@ -41935,11 +42164,16 @@ function pickTargetStacks(stacks, pattern) {
|
|
|
41935
42164
|
function uniqueLambdaIds(routes, routesWithAuth) {
|
|
41936
42165
|
const seen = /* @__PURE__ */ new Set();
|
|
41937
42166
|
const out = [];
|
|
41938
|
-
for (const r of routes)
|
|
41939
|
-
|
|
41940
|
-
|
|
42167
|
+
for (const r of routes) {
|
|
42168
|
+
if (r.unsupported || r.mockCors) continue;
|
|
42169
|
+
if (r.lambdaLogicalId.length === 0) continue;
|
|
42170
|
+
if (!seen.has(r.lambdaLogicalId)) {
|
|
42171
|
+
seen.add(r.lambdaLogicalId);
|
|
42172
|
+
out.push(r.lambdaLogicalId);
|
|
42173
|
+
}
|
|
41941
42174
|
}
|
|
41942
42175
|
for (const entry of routesWithAuth) {
|
|
42176
|
+
if (entry.route.unsupported || entry.route.mockCors) continue;
|
|
41943
42177
|
const auth = entry.authorizer;
|
|
41944
42178
|
if (!auth) continue;
|
|
41945
42179
|
if (auth.kind === "lambda-token" || auth.kind === "lambda-request") {
|
|
@@ -42138,6 +42372,13 @@ function resolveAssetCodePath(stack, logicalId, resource) {
|
|
|
42138
42372
|
/**
|
|
42139
42373
|
* Print the discovered route table to stdout. Format mirrors the spec
|
|
42140
42374
|
* doc's example so verify.sh / users can read it at a glance.
|
|
42375
|
+
*
|
|
42376
|
+
* Routes with `unsupported` or `mockCors` are annotated so the user can
|
|
42377
|
+
* tell at a glance which routes will dispatch to a Lambda vs which
|
|
42378
|
+
* return 501 / 204 directly:
|
|
42379
|
+
* - normal: `GET /items -> Handler (HTTP API)`
|
|
42380
|
+
* - mockCors: `OPTIONS /items -> [MOCK CORS preflight] (REST v1, stage 'prod')`
|
|
42381
|
+
* - unsupported: `POST /admin -> [501 Not Implemented] (HTTP API)`
|
|
42141
42382
|
*/
|
|
42142
42383
|
function printRouteTable(routes) {
|
|
42143
42384
|
const sorted = [...routes.map((r) => r.route)].sort((a, b) => {
|
|
@@ -42149,7 +42390,8 @@ function printRouteTable(routes) {
|
|
|
42149
42390
|
process.stdout.write("Discovered routes:\n");
|
|
42150
42391
|
for (const r of sorted) {
|
|
42151
42392
|
const sourceLabel = r.source === "http-api" ? "HTTP API" : r.source === "rest-v1" ? `REST v1, stage '${r.stage}'` : "Function URL";
|
|
42152
|
-
|
|
42393
|
+
const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.lambdaLogicalId;
|
|
42394
|
+
process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${target} (${sourceLabel})\n`);
|
|
42153
42395
|
}
|
|
42154
42396
|
process.stdout.write("\n");
|
|
42155
42397
|
}
|
|
@@ -42292,6 +42534,21 @@ function printPerServerRouteTables(servers) {
|
|
|
42292
42534
|
}
|
|
42293
42535
|
}
|
|
42294
42536
|
/**
|
|
42537
|
+
* Surface every `unsupported` route (deferred 501) as a startup warn so
|
|
42538
|
+
* the user sees what isn't reachable BEFORE they try to curl it. One
|
|
42539
|
+
* warn line per route — the route's `unsupported.reason` already names
|
|
42540
|
+
* the offender + the underlying limitation, so we just prefix with
|
|
42541
|
+
* method + path. Returns the number of unsupported routes so the caller
|
|
42542
|
+
* can emit a single-line summary header above the list.
|
|
42543
|
+
*/
|
|
42544
|
+
function warnUnsupportedRoutes(routes, logger) {
|
|
42545
|
+
const unsupported = routes.filter((r) => r.unsupported);
|
|
42546
|
+
if (unsupported.length === 0) return 0;
|
|
42547
|
+
logger.warn(`${unsupported.length} route(s) will respond HTTP 501 Not Implemented when hit (boot continued):`);
|
|
42548
|
+
for (const r of unsupported) logger.warn(` - ${r.method} ${r.pathPattern}: ${r.unsupported.reason}`);
|
|
42549
|
+
return unsupported.length;
|
|
42550
|
+
}
|
|
42551
|
+
/**
|
|
42295
42552
|
* One reload cycle for the multi-server topology (issue #260). The
|
|
42296
42553
|
* watcher serializes calls via a chain promise; this function:
|
|
42297
42554
|
*
|
|
@@ -42331,13 +42588,16 @@ async function reloadAllServers(args) {
|
|
|
42331
42588
|
pool: newPool,
|
|
42332
42589
|
corsConfigByApiId: material.corsConfigByApiId
|
|
42333
42590
|
};
|
|
42334
|
-
booted.server.setServerState(newState)
|
|
42591
|
+
const previousState = booted.server.setServerState(newState);
|
|
42592
|
+
booted.group = group;
|
|
42593
|
+
previousState.pool.dispose().catch((err) => {
|
|
42335
42594
|
logger.debug(`Previous pool dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
42336
42595
|
});
|
|
42337
42596
|
}
|
|
42338
42597
|
lastAssetPaths.value = computeAssetPaths(material.specs);
|
|
42339
42598
|
if (watcher) watcher.update([output, ...lastAssetPaths.value]);
|
|
42340
42599
|
printPerServerRouteTables(servers);
|
|
42600
|
+
warnUnsupportedRoutes(servers.flatMap((s) => s.group.routes.map((r) => r.route)), logger);
|
|
42341
42601
|
}
|
|
42342
42602
|
/**
|
|
42343
42603
|
* Returns true when any value in the function's template env map is a
|
|
@@ -45300,7 +45560,7 @@ function reorderArgs(argv) {
|
|
|
45300
45560
|
*/
|
|
45301
45561
|
async function main() {
|
|
45302
45562
|
const program = new Command();
|
|
45303
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
45563
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.115.1");
|
|
45304
45564
|
program.addCommand(createBootstrapCommand());
|
|
45305
45565
|
program.addCommand(createSynthCommand());
|
|
45306
45566
|
program.addCommand(createListCommand());
|