@go-to-k/cdkd 0.114.1 → 0.115.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/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; hot reload via `--watch`; deploy-state-backed env var
463
- substitution via `--from-state`.
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$1(value) {
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
- * Throws {@link RouteDiscoveryError} on any unsupported shape with every
38323
- * offending route named in a single message.
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} unsupported route(s) in the synthesized template:\n` + errors.map((e) => ` - ${e}`).join("\n"));
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
- * Returns `[]` when the Method's integration is non-AWS_PROXY (e.g. MOCK,
38359
- * AWS, HTTP) that is a hard error, raised by the caller's catch.
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
- return [{
38381
- method: httpMethod,
38382
- pathPattern: path,
38383
- lambdaLogicalId,
38394
+ const baseRoute = {
38384
38395
  source: "rest-v1",
38385
38396
  apiVersion: "v1",
38386
38397
  stage,
@@ -38388,7 +38399,98 @@ 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.` }
38391
38436
  }];
38437
+ return [{
38438
+ ...baseRoute,
38439
+ method: httpMethod,
38440
+ pathPattern: path,
38441
+ lambdaLogicalId: arnOutcome.logicalId
38442
+ }];
38443
+ }
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
+ };
38392
38494
  }
38393
38495
  /**
38394
38496
  * Walk a chain of `AWS::ApiGateway::Resource` parent pointers up to the
@@ -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") throw new Error(`${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): WebSocket APIs are not supported in cdkd local start-api (deferred follow-up PR).`);
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 apiCdkPath = readApiCdkPath(apiLogicalId, template);
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
- * C12: keep only `AuthType === 'NONE' && InvokeMode !== 'RESPONSE_STREAM'`.
38500
- * Other shapes hard-fail at discovery IAM auth needs SigV4 verification
38501
- * we cannot do locally, and RESPONSE_STREAM uses a streaming response shape
38502
- * (`InvokeWithResponseStream`) the RIE container does not implement.
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 lambdaLogicalId = resolveLambdaArnIntrinsic(targetArn, `${stackName}/${logicalId}.TargetFunctionArn`);
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
- return [{
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
- * Any other shape hard-errors with the offending route + raw intrinsic
38545
- * named, preserving the pre-extraction error message intent ("requires
38546
- * deploy-state and is not supported in cdkd local start-api"). The
38547
- * shared helper returns a discriminated union so the caller wraps the
38548
- * unsupported case with its own error class; this site uses bare `Error`
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 resolveLambdaArnIntrinsic(value, location) {
38559
- const outcome = resolveLambdaArnIntrinsic$1(value);
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.
@@ -39850,7 +39997,7 @@ function resolveHttpApiAuthorizer(authorizerLogicalId, routeAuthorizationScopes,
39850
39997
  * the offending route + raw intrinsic named.
39851
39998
  */
39852
39999
  function resolveLambdaArn(value, location) {
39853
- const outcome = resolveLambdaArnIntrinsic$1(value);
40000
+ const outcome = resolveLambdaArnIntrinsic(value);
39854
40001
  if (outcome.kind === "resolved") return outcome.logicalId;
39855
40002
  throw new RouteDiscoveryError(`${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
40003
  }
@@ -40024,6 +40171,10 @@ function attachAuthorizers(stacks, routes) {
40024
40171
  const out = [];
40025
40172
  const errors = [];
40026
40173
  for (const route of routes) {
40174
+ if (route.unsupported || route.mockCors) {
40175
+ out.push({ route });
40176
+ continue;
40177
+ }
40027
40178
  const stack = stackByRoute.get(route.declaredAt);
40028
40179
  if (!stack) {
40029
40180
  out.push({ route });
@@ -40778,6 +40929,14 @@ async function handleRequest(req, res, state, opts) {
40778
40929
  writeError(res, 404, "{\"message\":\"Not Found\"}");
40779
40930
  return;
40780
40931
  }
40932
+ if (match.route.mockCors) {
40933
+ writeMockCorsPreflight(res, match.route.mockCors);
40934
+ return;
40935
+ }
40936
+ if (match.route.unsupported) {
40937
+ writeNotImplemented(res, match.route.unsupported.reason);
40938
+ return;
40939
+ }
40781
40940
  const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
40782
40941
  const snapshot = {
40783
40942
  method,
@@ -41191,6 +41350,35 @@ function writeError(res, statusCode, body = "{\"message\":\"Internal server erro
41191
41350
  res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
41192
41351
  res.end(body);
41193
41352
  }
41353
+ /**
41354
+ * Write the 501 Not Implemented response surfaced for routes the
41355
+ * discovery layer flagged as `unsupported`. The integration's reason
41356
+ * (e.g. "MOCK integration is not emulated", "WebSocket APIs are not
41357
+ * supported") is echoed in the body so the user gets a precise pointer
41358
+ * at first hit instead of a generic 502.
41359
+ */
41360
+ function writeNotImplemented(res, reason) {
41361
+ const body = JSON.stringify({
41362
+ message: "Not Implemented",
41363
+ reason
41364
+ });
41365
+ res.statusCode = 501;
41366
+ res.setHeader("content-type", "application/json");
41367
+ res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
41368
+ res.end(body);
41369
+ }
41370
+ /**
41371
+ * Write the canonical CORS preflight response derived from a REST v1
41372
+ * MOCK Method's `Integration.IntegrationResponses[0].ResponseParameters`.
41373
+ * Headers are emitted verbatim — the discovery layer already stripped
41374
+ * AWS's literal single-quote wrappers and dropped any non-literal
41375
+ * (intrinsic-valued) entries.
41376
+ */
41377
+ function writeMockCorsPreflight(res, preflight) {
41378
+ res.statusCode = preflight.statusCode;
41379
+ for (const [name, value] of Object.entries(preflight.headers)) res.setHeader(name, value);
41380
+ res.end();
41381
+ }
41194
41382
 
41195
41383
  //#endregion
41196
41384
  //#region src/local/api-server-grouping.ts
@@ -41817,6 +42005,7 @@ async function localStartApiCommand(target, options) {
41817
42005
  if (basePort !== 0) nextPort += 1;
41818
42006
  }
41819
42007
  printPerServerRouteTables(servers);
42008
+ warnUnsupportedRoutes(servers.flatMap((s) => s.group.routes.map((r) => r.route)), logger);
41820
42009
  logger.info(`Per-Lambda concurrency: ${perLambdaConcurrency} (override with --per-lambda-concurrency)`);
41821
42010
  for (const { group, server } of servers) process.stdout.write(`Server listening on http://${server.host}:${server.port} (${group.displayName})\n`);
41822
42011
  process.stdout.write("^C to stop and clean up containers.\n");
@@ -41935,11 +42124,16 @@ function pickTargetStacks(stacks, pattern) {
41935
42124
  function uniqueLambdaIds(routes, routesWithAuth) {
41936
42125
  const seen = /* @__PURE__ */ new Set();
41937
42126
  const out = [];
41938
- for (const r of routes) if (!seen.has(r.lambdaLogicalId)) {
41939
- seen.add(r.lambdaLogicalId);
41940
- out.push(r.lambdaLogicalId);
42127
+ for (const r of routes) {
42128
+ if (r.unsupported || r.mockCors) continue;
42129
+ if (r.lambdaLogicalId.length === 0) continue;
42130
+ if (!seen.has(r.lambdaLogicalId)) {
42131
+ seen.add(r.lambdaLogicalId);
42132
+ out.push(r.lambdaLogicalId);
42133
+ }
41941
42134
  }
41942
42135
  for (const entry of routesWithAuth) {
42136
+ if (entry.route.unsupported || entry.route.mockCors) continue;
41943
42137
  const auth = entry.authorizer;
41944
42138
  if (!auth) continue;
41945
42139
  if (auth.kind === "lambda-token" || auth.kind === "lambda-request") {
@@ -42138,6 +42332,13 @@ function resolveAssetCodePath(stack, logicalId, resource) {
42138
42332
  /**
42139
42333
  * Print the discovered route table to stdout. Format mirrors the spec
42140
42334
  * doc's example so verify.sh / users can read it at a glance.
42335
+ *
42336
+ * Routes with `unsupported` or `mockCors` are annotated so the user can
42337
+ * tell at a glance which routes will dispatch to a Lambda vs which
42338
+ * return 501 / 204 directly:
42339
+ * - normal: `GET /items -> Handler (HTTP API)`
42340
+ * - mockCors: `OPTIONS /items -> [MOCK CORS preflight] (REST v1, stage 'prod')`
42341
+ * - unsupported: `POST /admin -> [501 Not Implemented] (HTTP API)`
42141
42342
  */
42142
42343
  function printRouteTable(routes) {
42143
42344
  const sorted = [...routes.map((r) => r.route)].sort((a, b) => {
@@ -42149,7 +42350,8 @@ function printRouteTable(routes) {
42149
42350
  process.stdout.write("Discovered routes:\n");
42150
42351
  for (const r of sorted) {
42151
42352
  const sourceLabel = r.source === "http-api" ? "HTTP API" : r.source === "rest-v1" ? `REST v1, stage '${r.stage}'` : "Function URL";
42152
- process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${r.lambdaLogicalId} (${sourceLabel})\n`);
42353
+ const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.lambdaLogicalId;
42354
+ process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${target} (${sourceLabel})\n`);
42153
42355
  }
42154
42356
  process.stdout.write("\n");
42155
42357
  }
@@ -42292,6 +42494,21 @@ function printPerServerRouteTables(servers) {
42292
42494
  }
42293
42495
  }
42294
42496
  /**
42497
+ * Surface every `unsupported` route (deferred 501) as a startup warn so
42498
+ * the user sees what isn't reachable BEFORE they try to curl it. One
42499
+ * warn line per route — the route's `unsupported.reason` already names
42500
+ * the offender + the underlying limitation, so we just prefix with
42501
+ * method + path. Returns the number of unsupported routes so the caller
42502
+ * can emit a single-line summary header above the list.
42503
+ */
42504
+ function warnUnsupportedRoutes(routes, logger) {
42505
+ const unsupported = routes.filter((r) => r.unsupported);
42506
+ if (unsupported.length === 0) return 0;
42507
+ logger.warn(`${unsupported.length} route(s) will respond HTTP 501 Not Implemented when hit (boot continued):`);
42508
+ for (const r of unsupported) logger.warn(` - ${r.method} ${r.pathPattern}: ${r.unsupported.reason}`);
42509
+ return unsupported.length;
42510
+ }
42511
+ /**
42295
42512
  * One reload cycle for the multi-server topology (issue #260). The
42296
42513
  * watcher serializes calls via a chain promise; this function:
42297
42514
  *
@@ -42331,13 +42548,16 @@ async function reloadAllServers(args) {
42331
42548
  pool: newPool,
42332
42549
  corsConfigByApiId: material.corsConfigByApiId
42333
42550
  };
42334
- booted.server.setServerState(newState).pool.dispose().catch((err) => {
42551
+ const previousState = booted.server.setServerState(newState);
42552
+ booted.group = group;
42553
+ previousState.pool.dispose().catch((err) => {
42335
42554
  logger.debug(`Previous pool dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`);
42336
42555
  });
42337
42556
  }
42338
42557
  lastAssetPaths.value = computeAssetPaths(material.specs);
42339
42558
  if (watcher) watcher.update([output, ...lastAssetPaths.value]);
42340
42559
  printPerServerRouteTables(servers);
42560
+ warnUnsupportedRoutes(servers.flatMap((s) => s.group.routes.map((r) => r.route)), logger);
42341
42561
  }
42342
42562
  /**
42343
42563
  * Returns true when any value in the function's template env map is a
@@ -45300,7 +45520,7 @@ function reorderArgs(argv) {
45300
45520
  */
45301
45521
  async function main() {
45302
45522
  const program = new Command();
45303
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.114.1");
45523
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.115.0");
45304
45524
  program.addCommand(createBootstrapCommand());
45305
45525
  program.addCommand(createSynthCommand());
45306
45526
  program.addCommand(createListCommand());