@go-to-k/cdkd 0.72.0 → 0.74.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/dist/cli.js CHANGED
@@ -70034,7 +70034,7 @@ async function captureObservedForImportedResources(stackState, providerRegistry,
70034
70034
  }
70035
70035
 
70036
70036
  // src/cli/commands/local-invoke.ts
70037
- import { mkdtempSync as mkdtempSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
70037
+ import { cpSync as cpSync2, mkdtempSync as mkdtempSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
70038
70038
  import { tmpdir as tmpdir3 } from "node:os";
70039
70039
  import { dirname as dirname3 } from "node:path";
70040
70040
  import * as path2 from "node:path";
@@ -70169,6 +70169,7 @@ function extractLambdaProperties(stack, logicalId, resource) {
70169
70169
  if (!inlineCode) {
70170
70170
  codePath = resolveAssetCodePath(stack, logicalId, resource);
70171
70171
  }
70172
+ const layers = resolveLambdaLayers(stack, logicalId, props);
70172
70173
  return {
70173
70174
  kind: "zip",
70174
70175
  stack,
@@ -70179,6 +70180,7 @@ function extractLambdaProperties(stack, logicalId, resource) {
70179
70180
  memoryMb,
70180
70181
  timeoutSec,
70181
70182
  codePath,
70183
+ layers,
70182
70184
  ...inlineCode !== void 0 && { inlineCode }
70183
70185
  };
70184
70186
  }
@@ -70235,7 +70237,8 @@ function extractImageLambdaProperties(args) {
70235
70237
  timeoutSec,
70236
70238
  imageUri,
70237
70239
  imageConfig,
70238
- architecture
70240
+ architecture,
70241
+ layers: []
70239
70242
  };
70240
70243
  }
70241
70244
  function resolveAssetCodePath(stack, logicalId, resource) {
@@ -70255,6 +70258,72 @@ function resolveAssetCodePath(stack, logicalId, resource) {
70255
70258
  }
70256
70259
  return abs;
70257
70260
  }
70261
+ function resolveLambdaLayers(stack, logicalId, props) {
70262
+ const layers = props["Layers"];
70263
+ if (layers === void 0)
70264
+ return [];
70265
+ if (!Array.isArray(layers)) {
70266
+ throw new LocalInvokeResolutionError(
70267
+ `Lambda '${logicalId}' has a non-array Layers property. Expected an array of LayerVersion references.`
70268
+ );
70269
+ }
70270
+ if (layers.length === 0)
70271
+ return [];
70272
+ const resources = stack.template.Resources ?? {};
70273
+ const out = [];
70274
+ for (let i = 0; i < layers.length; i++) {
70275
+ const entry = layers[i];
70276
+ const layerLogicalId = pickLayerLogicalId(entry);
70277
+ if (!layerLogicalId) {
70278
+ throw new LocalInvokeResolutionError(
70279
+ `Lambda '${logicalId}' has a Layers entry [${i}] cdkd cannot resolve locally: ${describeLayerEntry(entry)}. Only same-stack Ref / Fn::GetAtt to an AWS::Lambda::LayerVersion are supported in v1; cross-account / cross-region / pre-existing-ARN layers are deferred to a follow-up PR.`
70280
+ );
70281
+ }
70282
+ const layerResource = resources[layerLogicalId];
70283
+ if (!layerResource) {
70284
+ throw new LocalInvokeResolutionError(
70285
+ `Lambda '${logicalId}' Layers entry [${i}] references '${layerLogicalId}', but no resource with that logical ID exists in stack '${stack.stackName}'.`
70286
+ );
70287
+ }
70288
+ if (layerResource.Type !== "AWS::Lambda::LayerVersion") {
70289
+ throw new LocalInvokeResolutionError(
70290
+ `Lambda '${logicalId}' Layers entry [${i}] references '${layerLogicalId}' (${layerResource.Type}), which is not an AWS::Lambda::LayerVersion.`
70291
+ );
70292
+ }
70293
+ const assetPath = resolveAssetCodePath(stack, layerLogicalId, layerResource);
70294
+ out.push({ logicalId: layerLogicalId, assetPath });
70295
+ }
70296
+ return out;
70297
+ }
70298
+ function pickLayerLogicalId(entry) {
70299
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
70300
+ return void 0;
70301
+ const obj = entry;
70302
+ if (typeof obj["Ref"] === "string")
70303
+ return obj["Ref"];
70304
+ if ("Fn::GetAtt" in obj) {
70305
+ const arg = obj["Fn::GetAtt"];
70306
+ if (Array.isArray(arg) && typeof arg[0] === "string")
70307
+ return arg[0];
70308
+ if (typeof arg === "string")
70309
+ return arg.split(".")[0];
70310
+ }
70311
+ return void 0;
70312
+ }
70313
+ function describeLayerEntry(entry) {
70314
+ if (typeof entry === "string")
70315
+ return `literal ARN '${entry}'`;
70316
+ if (entry === null)
70317
+ return "null";
70318
+ if (typeof entry !== "object")
70319
+ return String(entry);
70320
+ try {
70321
+ const json = JSON.stringify(entry);
70322
+ return json.length > 120 ? json.substring(0, 117) + "..." : json;
70323
+ } catch {
70324
+ return Object.prototype.toString.call(entry);
70325
+ }
70326
+ }
70258
70327
  function notFoundError(target, stack, resources) {
70259
70328
  const lambdas = [];
70260
70329
  for (const [logicalId, resource] of Object.entries(resources)) {
@@ -70578,6 +70647,12 @@ async function runDetached(opts) {
70578
70647
  const ro = mount.readOnly ? ":ro" : "";
70579
70648
  args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
70580
70649
  }
70650
+ if (opts.extraMounts) {
70651
+ for (const mount of opts.extraMounts) {
70652
+ const ro = mount.readOnly ? ":ro" : "";
70653
+ args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
70654
+ }
70655
+ }
70581
70656
  for (const [k, v] of Object.entries(opts.env)) {
70582
70657
  args.push("-e", `${k}=${v}`);
70583
70658
  }
@@ -71089,7 +71164,7 @@ function extractHashFromImageUri(imageUri) {
71089
71164
  init_aws_clients();
71090
71165
 
71091
71166
  // src/cli/commands/local-start-api.ts
71092
- import { mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
71167
+ import { cpSync, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
71093
71168
  import { tmpdir as tmpdir2 } from "node:os";
71094
71169
  import * as path from "node:path";
71095
71170
  import { Command as Command14, Option as Option7 } from "commander";
@@ -71480,9 +71555,11 @@ function createContainerPool(specs, options) {
71480
71555
  logger.debug(
71481
71556
  `Starting container ${name} for ${spec.lambda.logicalId} on ${spec.containerHost}:${hostPort}`
71482
71557
  );
71558
+ const optMount = spec.optDir ? [{ hostPath: spec.optDir, containerPath: "/opt", readOnly: true }] : [];
71483
71559
  const containerId = await runDetached({
71484
71560
  image,
71485
71561
  mounts: [{ hostPath: spec.codeDir, containerPath: "/var/task", readOnly: true }],
71562
+ extraMounts: optMount,
71486
71563
  env: spec.env,
71487
71564
  cmd: [spec.lambda.handler],
71488
71565
  hostPort,
@@ -71778,6 +71855,48 @@ function buildRestV1Event(req, ctx, opts = {}) {
71778
71855
  };
71779
71856
  return event;
71780
71857
  }
71858
+ function applyAuthorizerOverlay(event, overlay) {
71859
+ const requestContext = event["requestContext"] ?? {};
71860
+ let authorizer;
71861
+ switch (overlay.kind) {
71862
+ case "lambda-rest-v1": {
71863
+ authorizer = {
71864
+ ...overlay.principalId !== void 0 && { principalId: overlay.principalId },
71865
+ ...overlay.context ?? {}
71866
+ };
71867
+ break;
71868
+ }
71869
+ case "lambda-http-v2": {
71870
+ authorizer = {
71871
+ lambda: {
71872
+ ...overlay.principalId !== void 0 && { principalId: overlay.principalId },
71873
+ ...overlay.context ?? {}
71874
+ }
71875
+ };
71876
+ break;
71877
+ }
71878
+ case "cognito-rest-v1": {
71879
+ authorizer = { claims: { ...overlay.claims } };
71880
+ break;
71881
+ }
71882
+ case "jwt-http-v2": {
71883
+ authorizer = {
71884
+ jwt: {
71885
+ claims: { ...overlay.claims },
71886
+ scopes: overlay.scopes ?? []
71887
+ }
71888
+ };
71889
+ break;
71890
+ }
71891
+ }
71892
+ return {
71893
+ ...event,
71894
+ requestContext: {
71895
+ ...requestContext,
71896
+ authorizer
71897
+ }
71898
+ };
71899
+ }
71781
71900
  function splitRawUrl(rawUrl) {
71782
71901
  const q = rawUrl.indexOf("?");
71783
71902
  if (q === -1)
@@ -72144,154 +72263,1257 @@ function isPlaceholder(segment) {
72144
72263
  return /^\{[^/{}+]+\}$/.test(segment);
72145
72264
  }
72146
72265
 
72147
- // src/local/http-server.ts
72148
- async function startApiServer(opts) {
72149
- const logger = getLogger().child("start-api");
72150
- const server = createServer2((req, res) => {
72151
- handleRequest(req, res, opts).catch((err) => {
72152
- logger.error(
72153
- `Unhandled request error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
72266
+ // src/local/authorizer-resolver.ts
72267
+ function resolveRestV1Authorizer(authorizerLogicalId, template, stackName, declaredAt) {
72268
+ const authResource = template.Resources?.[authorizerLogicalId];
72269
+ if (!authResource || authResource.Type !== "AWS::ApiGateway::Authorizer") {
72270
+ throw new RouteDiscoveryError(
72271
+ `${declaredAt}: AuthorizerId '${authorizerLogicalId}' does not point at an AWS::ApiGateway::Authorizer in stack '${stackName}'.`
72272
+ );
72273
+ }
72274
+ const props = authResource.Properties ?? {};
72275
+ const type = props["Type"];
72276
+ if (type === "TOKEN") {
72277
+ const lambdaLogicalId = resolveLambdaArn(
72278
+ props["AuthorizerUri"],
72279
+ `${stackName}/${authorizerLogicalId}.AuthorizerUri`
72280
+ );
72281
+ const identitySource = typeof props["IdentitySource"] === "string" ? props["IdentitySource"] : "method.request.header.Authorization";
72282
+ const tokenHeader = parseRestV1HeaderSelector(identitySource, stackName, authorizerLogicalId);
72283
+ const ttl = parseTtl(props["AuthorizerResultTtlInSeconds"], 300, 3600);
72284
+ return {
72285
+ kind: "lambda-token",
72286
+ logicalId: authorizerLogicalId,
72287
+ lambdaLogicalId,
72288
+ tokenHeader,
72289
+ resultTtlSeconds: ttl,
72290
+ declaredAt
72291
+ };
72292
+ }
72293
+ if (type === "REQUEST") {
72294
+ const lambdaLogicalId = resolveLambdaArn(
72295
+ props["AuthorizerUri"],
72296
+ `${stackName}/${authorizerLogicalId}.AuthorizerUri`
72297
+ );
72298
+ const identitySources = parseRestV1IdentitySources(
72299
+ typeof props["IdentitySource"] === "string" ? props["IdentitySource"] : ""
72300
+ );
72301
+ const ttl = parseTtl(props["AuthorizerResultTtlInSeconds"], 300, 3600);
72302
+ return {
72303
+ kind: "lambda-request",
72304
+ logicalId: authorizerLogicalId,
72305
+ lambdaLogicalId,
72306
+ identitySources,
72307
+ resultTtlSeconds: ttl,
72308
+ apiVersion: "v1",
72309
+ declaredAt
72310
+ };
72311
+ }
72312
+ if (type === "COGNITO_USER_POOLS") {
72313
+ const arns = props["ProviderARNs"];
72314
+ if (!Array.isArray(arns) || arns.length === 0) {
72315
+ throw new RouteDiscoveryError(
72316
+ `${stackName}/${authorizerLogicalId}: COGNITO_USER_POOLS authorizer is missing ProviderARNs.`
72154
72317
  );
72155
- if (!res.headersSent) {
72156
- writeError(res, 502);
72157
- }
72158
- });
72159
- });
72160
- server.on("connection", (socket) => {
72161
- socket.setNoDelay(true);
72162
- });
72163
- const { actualPort, actualHost } = await new Promise(
72164
- (resolveListen, rejectListen) => {
72165
- server.once("error", rejectListen);
72166
- server.listen(opts.port, opts.host, () => {
72167
- const addr = server.address();
72168
- if (addr === null || typeof addr === "string") {
72169
- rejectListen(new Error("Could not determine listening address"));
72170
- return;
72171
- }
72172
- resolveListen({ actualPort: addr.port, actualHost: opts.host });
72173
- });
72174
72318
  }
72319
+ const arn = pickStringFromArn(arns[0], `${stackName}/${authorizerLogicalId}.ProviderARNs[0]`);
72320
+ const parsed = parseCognitoUserPoolArn(arn, `${stackName}/${authorizerLogicalId}`);
72321
+ return {
72322
+ kind: "cognito",
72323
+ logicalId: authorizerLogicalId,
72324
+ userPoolArn: arn,
72325
+ region: parsed.region,
72326
+ userPoolId: parsed.userPoolId,
72327
+ declaredAt
72328
+ };
72329
+ }
72330
+ throw new RouteDiscoveryError(
72331
+ `${stackName}/${authorizerLogicalId}: AWS::ApiGateway::Authorizer.Type '${String(type)}' is not supported by cdkd local start-api (only TOKEN / REQUEST / COGNITO_USER_POOLS \u2014 IAM / mTLS authorizers are deferred to a follow-up PR).`
72175
72332
  );
72176
- let closed = false;
72177
- return {
72178
- port: actualPort,
72179
- host: actualHost,
72180
- server,
72181
- close: async () => {
72182
- if (closed)
72183
- return;
72184
- closed = true;
72185
- await new Promise((resolveClose) => {
72186
- server.close(() => resolveClose());
72187
- server.closeAllConnections?.();
72188
- });
72189
- }
72190
- };
72191
72333
  }
72192
- async function handleRequest(req, res, opts) {
72193
- const logger = getLogger().child("start-api");
72194
- const bodyBuf = await readBody(req);
72195
- const rawUrl = req.url ?? "/";
72196
- const method = (req.method ?? "GET").toUpperCase();
72197
- const requestPath = rawUrl.split("?")[0] ?? "/";
72198
- const match = matchRoute(method, requestPath, opts.routes);
72199
- if (!match) {
72200
- writeError(res, 404, '{"message":"Not Found"}');
72201
- return;
72202
- }
72203
- const snapshot = {
72204
- method,
72205
- rawUrl,
72206
- headers: collectHeaders(req),
72207
- body: bodyBuf,
72208
- ...req.socket.remoteAddress !== void 0 && { sourceIp: req.socket.remoteAddress }
72209
- };
72210
- const matchCtx = {
72211
- route: match.route,
72212
- pathParameters: match.pathParameters,
72213
- matchedPath: requestPath
72214
- };
72215
- const event = match.route.apiVersion === "v1" ? buildRestV1Event(snapshot, matchCtx) : buildHttpApiV2Event(snapshot, matchCtx);
72216
- let handle;
72217
- try {
72218
- handle = await opts.pool.acquire(match.route.lambdaLogicalId);
72219
- } catch (err) {
72220
- logger.error(
72221
- `Failed to acquire container for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`
72334
+ function resolveHttpApiAuthorizer(authorizerLogicalId, routeAuthorizationScopes, template, stackName, declaredAt) {
72335
+ const authResource = template.Resources?.[authorizerLogicalId];
72336
+ if (!authResource || authResource.Type !== "AWS::ApiGatewayV2::Authorizer") {
72337
+ throw new RouteDiscoveryError(
72338
+ `${declaredAt}: AuthorizerId '${authorizerLogicalId}' does not point at an AWS::ApiGatewayV2::Authorizer in stack '${stackName}'.`
72222
72339
  );
72223
- writeError(res, 502);
72224
- return;
72225
72340
  }
72226
- try {
72227
- const invokeResult = await invokeRie(
72228
- handle.containerHost,
72229
- handle.hostPort,
72230
- event,
72231
- opts.rieTimeoutMs
72341
+ const props = authResource.Properties ?? {};
72342
+ const authType = props["AuthorizerType"];
72343
+ if (authType === "REQUEST") {
72344
+ const lambdaLogicalId = resolveLambdaArn(
72345
+ props["AuthorizerUri"],
72346
+ `${stackName}/${authorizerLogicalId}.AuthorizerUri`
72232
72347
  );
72233
- const translated = translateLambdaResponse(invokeResult.payload, match.route.apiVersion);
72234
- res.statusCode = translated.statusCode;
72235
- for (const [name, value] of Object.entries(translated.headers)) {
72236
- res.setHeader(name, value);
72348
+ const identitySources = parseHttpV2IdentitySources(props["IdentitySource"]);
72349
+ const ttl = parseTtl(props["AuthorizerResultTtlInSeconds"], 0, 3600);
72350
+ return {
72351
+ kind: "lambda-request",
72352
+ logicalId: authorizerLogicalId,
72353
+ lambdaLogicalId,
72354
+ identitySources,
72355
+ resultTtlSeconds: ttl,
72356
+ apiVersion: "v2",
72357
+ declaredAt
72358
+ };
72359
+ }
72360
+ if (authType === "JWT") {
72361
+ const jwt = props["JwtConfiguration"];
72362
+ if (!jwt || typeof jwt !== "object") {
72363
+ throw new RouteDiscoveryError(
72364
+ `${stackName}/${authorizerLogicalId}: AWS::ApiGatewayV2::Authorizer.JwtConfiguration is required for AuthorizerType=JWT.`
72365
+ );
72237
72366
  }
72238
- if (translated.cookies.length > 0) {
72239
- res.setHeader("set-cookie", translated.cookies);
72367
+ const obj = jwt;
72368
+ const issuer = obj["Issuer"];
72369
+ if (typeof issuer !== "string" || issuer.length === 0) {
72370
+ throw new RouteDiscoveryError(
72371
+ `${stackName}/${authorizerLogicalId}: JwtConfiguration.Issuer must be a string.`
72372
+ );
72240
72373
  }
72241
- res.end(translated.body);
72242
- } catch (err) {
72243
- logger.error(
72244
- `RIE invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`
72245
- );
72246
- if (!res.headersSent) {
72247
- writeError(res, 502);
72248
- } else {
72249
- res.end();
72374
+ const audienceRaw = obj["Audience"];
72375
+ const audience = Array.isArray(audienceRaw) ? audienceRaw.filter((s) => typeof s === "string") : [];
72376
+ const cognito = parseCognitoIssuer(issuer);
72377
+ return {
72378
+ kind: "jwt",
72379
+ logicalId: authorizerLogicalId,
72380
+ issuer,
72381
+ audience,
72382
+ ...cognito && { region: cognito.region, userPoolId: cognito.userPoolId },
72383
+ declaredAt
72384
+ };
72385
+ }
72386
+ throw new RouteDiscoveryError(
72387
+ `${stackName}/${authorizerLogicalId}: AWS::ApiGatewayV2::Authorizer.AuthorizerType '${String(authType)}' is not supported by cdkd local start-api (only REQUEST / JWT).`
72388
+ );
72389
+ }
72390
+ function resolveLambdaArn(value, location) {
72391
+ if (value && typeof value === "object" && !Array.isArray(value)) {
72392
+ const obj = value;
72393
+ if ("Ref" in obj && typeof obj["Ref"] === "string")
72394
+ return obj["Ref"];
72395
+ if ("Fn::GetAtt" in obj) {
72396
+ const arg = obj["Fn::GetAtt"];
72397
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && arg[1] === "Arn") {
72398
+ return arg[0];
72399
+ }
72400
+ }
72401
+ if ("Fn::Join" in obj) {
72402
+ const join9 = obj["Fn::Join"];
72403
+ if (Array.isArray(join9) && join9.length === 2 && Array.isArray(join9[1])) {
72404
+ const parts = join9[1];
72405
+ const literal = parts.filter((p) => typeof p === "string").join("");
72406
+ if (literal.includes(":lambda:path/2015-03-31/functions/")) {
72407
+ for (const p of parts) {
72408
+ if (p && typeof p === "object" && !Array.isArray(p)) {
72409
+ const inner = p;
72410
+ const arg = inner["Fn::GetAtt"];
72411
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && arg[1] === "Arn") {
72412
+ return arg[0];
72413
+ }
72414
+ }
72415
+ }
72416
+ }
72417
+ }
72250
72418
  }
72251
- } finally {
72252
- opts.pool.release(handle);
72253
72419
  }
72420
+ throw new RouteDiscoveryError(
72421
+ `${location}: only { Ref }, { Fn::GetAtt: [..., 'Arn'] }, or the REST v1 invoke-ARN Fn::Join wrapper are supported (got ${shortJson2(value)}).`
72422
+ );
72254
72423
  }
72255
- function readBody(req) {
72256
- return new Promise((resolveBody, rejectBody) => {
72257
- const chunks = [];
72258
- req.on("data", (chunk) => {
72259
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
72260
- });
72261
- req.on("end", () => resolveBody(Buffer.concat(chunks)));
72262
- req.on("error", rejectBody);
72263
- });
72424
+ function parseRestV1HeaderSelector(identitySource, stackName, authorizerLogicalId) {
72425
+ const m = /^method\.request\.header\.([A-Za-z0-9-_]+)$/.exec(identitySource.trim());
72426
+ if (!m) {
72427
+ throw new RouteDiscoveryError(
72428
+ `${stackName}/${authorizerLogicalId}: TOKEN authorizer IdentitySource '${identitySource}' must be 'method.request.header.<HeaderName>'.`
72429
+ );
72430
+ }
72431
+ return m[1].toLowerCase();
72264
72432
  }
72265
- function collectHeaders(req) {
72266
- const out = {};
72267
- for (const [name, value] of Object.entries(req.headers)) {
72268
- if (Array.isArray(value)) {
72269
- out[name] = value;
72270
- } else if (typeof value === "string") {
72271
- out[name] = [value];
72433
+ function parseRestV1IdentitySources(raw) {
72434
+ const out = [];
72435
+ for (const tokenRaw of raw.split(",")) {
72436
+ const token = tokenRaw.trim();
72437
+ if (token.length === 0)
72438
+ continue;
72439
+ const headerMatch = /^method\.request\.header\.([A-Za-z0-9-_]+)$/.exec(token);
72440
+ if (headerMatch) {
72441
+ out.push({ kind: "header", name: headerMatch[1].toLowerCase() });
72442
+ continue;
72443
+ }
72444
+ const queryMatch = /^method\.request\.querystring\.([A-Za-z0-9-_]+)$/.exec(token);
72445
+ if (queryMatch) {
72446
+ out.push({ kind: "query", name: queryMatch[1] });
72447
+ continue;
72448
+ }
72449
+ const contextMatch = /^context\.([A-Za-z0-9._-]+)$/.exec(token);
72450
+ if (contextMatch) {
72451
+ out.push({ kind: "context", name: contextMatch[1] });
72452
+ continue;
72453
+ }
72454
+ const stageMatch = /^stageVariables\.([A-Za-z0-9._-]+)$/.exec(token);
72455
+ if (stageMatch) {
72456
+ out.push({ kind: "stage-variable", name: stageMatch[1] });
72457
+ continue;
72272
72458
  }
72459
+ out.push({ kind: "header", name: token.toLowerCase() });
72273
72460
  }
72274
72461
  return out;
72275
72462
  }
72276
- function writeError(res, statusCode, body = '{"message":"Internal server error"}') {
72277
- res.statusCode = statusCode;
72278
- res.setHeader("content-type", "application/json");
72279
- res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
72280
- res.end(body);
72463
+ function parseHttpV2IdentitySources(raw) {
72464
+ if (!Array.isArray(raw))
72465
+ return [];
72466
+ const out = [];
72467
+ for (const entry of raw) {
72468
+ if (typeof entry !== "string")
72469
+ continue;
72470
+ const headerMatch = /^\$request\.header\.([A-Za-z0-9-_]+)$/.exec(entry);
72471
+ if (headerMatch) {
72472
+ out.push({ kind: "header", name: headerMatch[1].toLowerCase() });
72473
+ continue;
72474
+ }
72475
+ const queryMatch = /^\$request\.querystring\.([A-Za-z0-9-_]+)$/.exec(entry);
72476
+ if (queryMatch) {
72477
+ out.push({ kind: "query", name: queryMatch[1] });
72478
+ continue;
72479
+ }
72480
+ out.push({ kind: "header", name: entry.toLowerCase() });
72481
+ }
72482
+ return out;
72281
72483
  }
72282
-
72283
- // src/cli/commands/local-start-api.ts
72284
- async function localStartApiCommand(options) {
72285
- const logger = getLogger();
72286
- if (options.verbose) {
72287
- logger.setLevel("debug");
72484
+ function parseCognitoUserPoolArn(arn, location) {
72485
+ const m = /^arn:aws[a-z0-9-]*:cognito-idp:([a-z0-9-]+):[0-9]+:userpool\/(.+)$/.exec(arn);
72486
+ if (!m) {
72487
+ throw new RouteDiscoveryError(
72488
+ `${location}: malformed Cognito User Pool ARN '${arn}'. Expected 'arn:aws:cognito-idp:<region>:<account>:userpool/<id>'.`
72489
+ );
72288
72490
  }
72289
- warnIfDeprecatedRegion(options);
72290
- await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
72291
- await ensureDockerAvailable();
72292
- const appCmd = resolveApp(options.app);
72293
- if (!appCmd) {
72294
- throw new Error('No CDK app specified. Pass --app, set CDKD_APP, or add "app" to cdk.json.');
72491
+ return { region: m[1], userPoolId: m[2] };
72492
+ }
72493
+ function parseCognitoIssuer(issuer) {
72494
+ const m = /^https:\/\/cognito-idp\.([a-z0-9-]+)\.amazonaws\.com\/([^/]+)\/?$/.exec(issuer);
72495
+ if (!m)
72496
+ return void 0;
72497
+ return { region: m[1], userPoolId: m[2] };
72498
+ }
72499
+ function pickStringFromArn(value, location) {
72500
+ if (typeof value === "string")
72501
+ return value;
72502
+ if (value && typeof value === "object" && !Array.isArray(value)) {
72503
+ const obj = value;
72504
+ if ("Fn::GetAtt" in obj) {
72505
+ const arg = obj["Fn::GetAtt"];
72506
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && arg[1] === "Arn") {
72507
+ throw new RouteDiscoveryError(
72508
+ `${location}: ProviderARNs[0] uses Fn::GetAtt against logical ID '${arg[0]}'. cdkd local start-api needs the literal ARN string to derive the JWKS URL \u2014 set the user pool ARN explicitly via 'authorizer.providerArns' on the CDK construct, or upgrade to JWT (HTTP v2) which encodes the pool in the Issuer URL.`
72509
+ );
72510
+ }
72511
+ }
72512
+ }
72513
+ throw new RouteDiscoveryError(
72514
+ `${location}: ProviderARNs[0] must be a literal string (got ${shortJson2(value)}).`
72515
+ );
72516
+ }
72517
+ function parseTtl(raw, fallback, max) {
72518
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0)
72519
+ return fallback;
72520
+ return Math.min(Math.trunc(raw), max);
72521
+ }
72522
+ function buildIdentityHash(parts) {
72523
+ return parts.map((p) => p ?? "").join("\0");
72524
+ }
72525
+ function attachAuthorizers(stacks, routes) {
72526
+ const stackByRoute = /* @__PURE__ */ new Map();
72527
+ for (const stack of stacks) {
72528
+ const prefix = `${stack.stackName}/`;
72529
+ for (const route of routes) {
72530
+ if (route.declaredAt.startsWith(prefix))
72531
+ stackByRoute.set(route.declaredAt, stack);
72532
+ }
72533
+ }
72534
+ const out = [];
72535
+ const errors = [];
72536
+ for (const route of routes) {
72537
+ const stack = stackByRoute.get(route.declaredAt);
72538
+ if (!stack) {
72539
+ out.push({ route });
72540
+ continue;
72541
+ }
72542
+ try {
72543
+ const authorizer = detectAuthorizer(route, stack);
72544
+ out.push({ route, ...authorizer && { authorizer } });
72545
+ } catch (err) {
72546
+ errors.push(err instanceof Error ? err.message : String(err));
72547
+ }
72548
+ }
72549
+ if (errors.length > 0) {
72550
+ throw new RouteDiscoveryError(
72551
+ `cdkd local start-api: ${errors.length} authorizer error(s):
72552
+ ` + errors.map((e) => ` - ${e}`).join("\n")
72553
+ );
72554
+ }
72555
+ return out;
72556
+ }
72557
+ function detectAuthorizer(route, stack) {
72558
+ const slash = route.declaredAt.indexOf("/");
72559
+ if (slash < 0)
72560
+ return void 0;
72561
+ const logicalId = route.declaredAt.slice(slash + 1);
72562
+ const resource = stack.template.Resources?.[logicalId];
72563
+ if (!resource)
72564
+ return void 0;
72565
+ if (resource.Type === "AWS::ApiGateway::Method") {
72566
+ return detectRestV1Authorizer(resource, logicalId, stack);
72567
+ }
72568
+ if (resource.Type === "AWS::ApiGatewayV2::Route") {
72569
+ return detectHttpApiAuthorizer(resource, logicalId, stack);
72570
+ }
72571
+ return void 0;
72572
+ }
72573
+ function detectRestV1Authorizer(methodResource, methodLogicalId, stack) {
72574
+ const props = methodResource.Properties ?? {};
72575
+ const authType = props["AuthorizationType"];
72576
+ if (authType === void 0 || authType === "NONE")
72577
+ return void 0;
72578
+ const authorizerId = props["AuthorizerId"];
72579
+ const refLogicalId = pickRefLogicalId2(authorizerId);
72580
+ if (authType === "AWS_IAM") {
72581
+ throw new RouteDiscoveryError(
72582
+ `${stack.stackName}/${methodLogicalId}: REST v1 AWS_IAM authorization is not supported by cdkd local start-api (deferred follow-up PR).`
72583
+ );
72584
+ }
72585
+ if (!refLogicalId) {
72586
+ throw new RouteDiscoveryError(
72587
+ `${stack.stackName}/${methodLogicalId}: AuthorizationType='${String(authType)}' but AuthorizerId is missing or not a {Ref:...}.`
72588
+ );
72589
+ }
72590
+ return resolveRestV1Authorizer(
72591
+ refLogicalId,
72592
+ stack.template,
72593
+ stack.stackName,
72594
+ `${stack.stackName}/${methodLogicalId}`
72595
+ );
72596
+ }
72597
+ function detectHttpApiAuthorizer(routeResource, routeLogicalId, stack) {
72598
+ const props = routeResource.Properties ?? {};
72599
+ const authType = props["AuthorizationType"];
72600
+ if (authType === void 0 || authType === "NONE")
72601
+ return void 0;
72602
+ const authorizerId = props["AuthorizerId"];
72603
+ const refLogicalId = pickRefLogicalId2(authorizerId);
72604
+ if (!refLogicalId) {
72605
+ throw new RouteDiscoveryError(
72606
+ `${stack.stackName}/${routeLogicalId}: AuthorizationType='${String(authType)}' but AuthorizerId is missing or not a {Ref:...}.`
72607
+ );
72608
+ }
72609
+ const scopesRaw = props["AuthorizationScopes"];
72610
+ const scopes = Array.isArray(scopesRaw) ? scopesRaw.filter((s) => typeof s === "string") : void 0;
72611
+ return resolveHttpApiAuthorizer(
72612
+ refLogicalId,
72613
+ scopes,
72614
+ stack.template,
72615
+ stack.stackName,
72616
+ `${stack.stackName}/${routeLogicalId}`
72617
+ );
72618
+ }
72619
+ function pickRefLogicalId2(value) {
72620
+ if (value && typeof value === "object" && !Array.isArray(value)) {
72621
+ const ref = value["Ref"];
72622
+ if (typeof ref === "string")
72623
+ return ref;
72624
+ }
72625
+ return null;
72626
+ }
72627
+ function shortJson2(value) {
72628
+ try {
72629
+ const s = JSON.stringify(value);
72630
+ return s.length > 200 ? `${s.slice(0, 200)}\u2026` : s;
72631
+ } catch {
72632
+ return String(value);
72633
+ }
72634
+ }
72635
+
72636
+ // src/local/lambda-authorizer.ts
72637
+ function buildMethodArn(opts) {
72638
+ const region = opts.region ?? "local";
72639
+ const trimmedPath = opts.path.replace(/^\//, "");
72640
+ return `arn:aws:execute-api:${region}:${opts.accountId}:${opts.apiId}/${opts.stage}/${opts.method.toUpperCase()}/${trimmedPath}`;
72641
+ }
72642
+ async function invokeTokenAuthorizer(authorizer, request, ctx) {
72643
+ const token = request.headers[authorizer.tokenHeader];
72644
+ if (!token || token.length === 0) {
72645
+ return { allow: false, identityHash: void 0 };
72646
+ }
72647
+ const event = {
72648
+ type: "TOKEN",
72649
+ authorizationToken: token,
72650
+ methodArn: ctx.methodArn
72651
+ };
72652
+ const identityHash = buildIdentityHash([token]);
72653
+ const result = await invokeAuthorizerLambda(authorizer.lambdaLogicalId, event, ctx);
72654
+ return parseLambdaAuthorizerResponse(result, ctx.methodArn, identityHash);
72655
+ }
72656
+ async function invokeRequestAuthorizer(authorizer, request, ctx) {
72657
+ const { identityHash, missing } = computeRequestIdentityHash(authorizer, request);
72658
+ if (missing) {
72659
+ return { allow: false, identityHash: void 0 };
72660
+ }
72661
+ const event = authorizer.apiVersion === "v1" ? buildRequestEventV1(authorizer, request, ctx) : buildRequestEventV2(authorizer, request, ctx);
72662
+ const result = await invokeAuthorizerLambda(authorizer.lambdaLogicalId, event, ctx);
72663
+ if (authorizer.apiVersion === "v2") {
72664
+ return parseHttpV2RequestResponse(result, ctx.methodArn, identityHash);
72665
+ }
72666
+ return parseLambdaAuthorizerResponse(result, ctx.methodArn, identityHash);
72667
+ }
72668
+ function extractIdentityValue(sel, request) {
72669
+ switch (sel.kind) {
72670
+ case "header":
72671
+ return request.headers[sel.name];
72672
+ case "query":
72673
+ return request.queryStringParameters[sel.name];
72674
+ case "context":
72675
+ return void 0;
72676
+ case "stage-variable":
72677
+ return void 0;
72678
+ }
72679
+ }
72680
+ function computeRequestIdentityHash(authorizer, request) {
72681
+ const identityValues = authorizer.identitySources.map(
72682
+ (sel) => extractIdentityValue(sel, request)
72683
+ );
72684
+ const missing = authorizer.apiVersion === "v1" && authorizer.identitySources.length > 0 && identityValues.every((v) => v === void 0 || v === "");
72685
+ return { identityHash: buildIdentityHash(identityValues), missing };
72686
+ }
72687
+ function buildRequestEventV1(authorizer, request, ctx) {
72688
+ return {
72689
+ type: "REQUEST",
72690
+ methodArn: ctx.methodArn,
72691
+ resource: request.matchedPath,
72692
+ path: request.matchedPath,
72693
+ httpMethod: request.method,
72694
+ headers: request.headers,
72695
+ multiValueHeaders: Object.fromEntries(
72696
+ Object.entries(request.headers).map(([k, v]) => [k, v.split(",")])
72697
+ ),
72698
+ queryStringParameters: request.queryStringParameters,
72699
+ multiValueQueryStringParameters: Object.fromEntries(
72700
+ Object.entries(request.queryStringParameters).map(([k, v]) => [k, [v]])
72701
+ ),
72702
+ pathParameters: request.pathParameters,
72703
+ stageVariables: null,
72704
+ requestContext: {
72705
+ accountId: ctx.mockAccountId,
72706
+ apiId: ctx.mockApiId,
72707
+ httpMethod: request.method,
72708
+ identity: { sourceIp: request.sourceIp },
72709
+ path: `/${request.stage}${request.matchedPath}`,
72710
+ stage: request.stage
72711
+ },
72712
+ authorizationToken: request.headers[authorizer.identitySources[0]?.name ?? "authorization"]
72713
+ };
72714
+ }
72715
+ function buildRequestEventV2(_authorizer, request, ctx) {
72716
+ return {
72717
+ version: "2.0",
72718
+ type: "REQUEST",
72719
+ routeArn: ctx.methodArn,
72720
+ identitySource: [],
72721
+ // Honored by AWS but not interpreted by user code.
72722
+ routeKey: `${request.method} ${request.matchedPath}`,
72723
+ rawPath: request.matchedPath,
72724
+ rawQueryString: "",
72725
+ headers: request.headers,
72726
+ queryStringParameters: request.queryStringParameters,
72727
+ pathParameters: request.pathParameters,
72728
+ stageVariables: null,
72729
+ requestContext: {
72730
+ accountId: ctx.mockAccountId,
72731
+ apiId: ctx.mockApiId,
72732
+ domainName: "localhost",
72733
+ domainPrefix: "local",
72734
+ http: {
72735
+ method: request.method,
72736
+ path: request.matchedPath,
72737
+ protocol: "HTTP/1.1",
72738
+ sourceIp: request.sourceIp,
72739
+ userAgent: request.headers["user-agent"] ?? ""
72740
+ },
72741
+ requestId: "local-authorizer",
72742
+ routeKey: `${request.method} ${request.matchedPath}`,
72743
+ stage: request.stage,
72744
+ time: "",
72745
+ timeEpoch: 0
72746
+ }
72747
+ };
72748
+ }
72749
+ async function invokeAuthorizerLambda(lambdaLogicalId, event, ctx) {
72750
+ const handle = await ctx.pool.acquire(lambdaLogicalId);
72751
+ try {
72752
+ const result = await invokeRie(handle.containerHost, handle.hostPort, event, ctx.rieTimeoutMs);
72753
+ return result.payload;
72754
+ } finally {
72755
+ ctx.pool.release(handle);
72756
+ }
72757
+ }
72758
+ function parseLambdaAuthorizerResponse(payload, methodArn, identityHash) {
72759
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
72760
+ return { allow: false, identityHash };
72761
+ }
72762
+ const obj = payload;
72763
+ const principalId = typeof obj["principalId"] === "string" ? obj["principalId"] : void 0;
72764
+ const context = obj["context"] && typeof obj["context"] === "object" && !Array.isArray(obj["context"]) ? obj["context"] : void 0;
72765
+ const policy = obj["policyDocument"];
72766
+ if (!policy || typeof policy !== "object") {
72767
+ return {
72768
+ allow: false,
72769
+ identityHash,
72770
+ ...principalId !== void 0 && { principalId },
72771
+ ...context && { context }
72772
+ };
72773
+ }
72774
+ const stmts = policy["Statement"];
72775
+ if (!Array.isArray(stmts)) {
72776
+ return {
72777
+ allow: false,
72778
+ identityHash,
72779
+ ...principalId !== void 0 && { principalId },
72780
+ ...context && { context },
72781
+ policy
72782
+ };
72783
+ }
72784
+ const allow = stmts.some((stmt) => {
72785
+ if (!stmt || typeof stmt !== "object" || Array.isArray(stmt))
72786
+ return false;
72787
+ const s = stmt;
72788
+ if (s["Effect"] !== "Allow")
72789
+ return false;
72790
+ const resources = Array.isArray(s["Resource"]) ? s["Resource"] : [s["Resource"]];
72791
+ return resources.some((r) => typeof r === "string" && resourceMatches(r, methodArn));
72792
+ });
72793
+ return {
72794
+ allow,
72795
+ identityHash,
72796
+ ...principalId !== void 0 && { principalId },
72797
+ ...context && { context },
72798
+ policy
72799
+ };
72800
+ }
72801
+ function parseHttpV2RequestResponse(payload, methodArn, identityHash) {
72802
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
72803
+ const obj = payload;
72804
+ if (typeof obj["isAuthorized"] === "boolean") {
72805
+ const context = obj["context"] && typeof obj["context"] === "object" && !Array.isArray(obj["context"]) ? obj["context"] : void 0;
72806
+ return {
72807
+ allow: obj["isAuthorized"],
72808
+ identityHash,
72809
+ ...context && { context }
72810
+ };
72811
+ }
72812
+ }
72813
+ return parseLambdaAuthorizerResponse(payload, methodArn, identityHash);
72814
+ }
72815
+ function resourceMatches(pattern, methodArn) {
72816
+ if (pattern === methodArn)
72817
+ return true;
72818
+ if (!pattern.includes("*") && !pattern.includes("?"))
72819
+ return false;
72820
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
72821
+ const re = new RegExp(`^${escaped}$`);
72822
+ return re.test(methodArn);
72823
+ }
72824
+ function evaluateCachedLambdaPolicy(cached, methodArn) {
72825
+ const policy = cached.policy;
72826
+ if (!policy || typeof policy !== "object") {
72827
+ return { ...cached, allow: false };
72828
+ }
72829
+ const stmts = policy["Statement"];
72830
+ if (!Array.isArray(stmts)) {
72831
+ return { ...cached, allow: false };
72832
+ }
72833
+ const allow = stmts.some((stmt) => {
72834
+ if (!stmt || typeof stmt !== "object" || Array.isArray(stmt))
72835
+ return false;
72836
+ const s = stmt;
72837
+ if (s["Effect"] !== "Allow")
72838
+ return false;
72839
+ const resources = Array.isArray(s["Resource"]) ? s["Resource"] : [s["Resource"]];
72840
+ return resources.some((r) => typeof r === "string" && resourceMatches(r, methodArn));
72841
+ });
72842
+ return { ...cached, allow };
72843
+ }
72844
+
72845
+ // src/local/cognito-jwt.ts
72846
+ import { createPublicKey, createVerify } from "node:crypto";
72847
+ var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
72848
+ var FAILURE_JWKS_TTL_MS = 60 * 1e3;
72849
+ function createJwksCache(opts = {}) {
72850
+ const fetchImpl = opts.fetchImpl ?? (async (url) => globalThis.fetch(url));
72851
+ const now = opts.now ?? (() => Date.now());
72852
+ const ttlMs = opts.ttlMs ?? DEFAULT_JWKS_TTL_MS;
72853
+ const failureTtlMs = opts.failureTtlMs ?? FAILURE_JWKS_TTL_MS;
72854
+ const map = /* @__PURE__ */ new Map();
72855
+ return {
72856
+ async fetchAndCache(jwksUrl) {
72857
+ const cached = map.get(jwksUrl);
72858
+ if (cached && cached.expiresAt > now())
72859
+ return cached;
72860
+ const logger = getLogger().child("cognito-jwt");
72861
+ try {
72862
+ const response = await fetchImpl(jwksUrl);
72863
+ if (!response.ok) {
72864
+ throw new Error(`JWKS fetch returned HTTP ${response.status}`);
72865
+ }
72866
+ const body = await response.text();
72867
+ const parsed = JSON.parse(body);
72868
+ const keys = Array.isArray(parsed.keys) ? parsed.keys : [];
72869
+ const byKid = /* @__PURE__ */ new Map();
72870
+ for (const k of keys) {
72871
+ if (!k || typeof k !== "object" || Array.isArray(k))
72872
+ continue;
72873
+ const obj = k;
72874
+ if (typeof obj["kid"] === "string" && typeof obj["n"] === "string" && typeof obj["e"] === "string" && typeof obj["kty"] === "string") {
72875
+ byKid.set(obj["kid"], {
72876
+ kid: obj["kid"],
72877
+ n: obj["n"],
72878
+ e: obj["e"],
72879
+ kty: obj["kty"],
72880
+ ...typeof obj["alg"] === "string" && { alg: obj["alg"] },
72881
+ ...typeof obj["use"] === "string" && { use: obj["use"] }
72882
+ });
72883
+ }
72884
+ }
72885
+ const entry = {
72886
+ byKid,
72887
+ expiresAt: now() + ttlMs,
72888
+ passThrough: false
72889
+ };
72890
+ map.set(jwksUrl, entry);
72891
+ return entry;
72892
+ } catch (err) {
72893
+ logger.warn(
72894
+ `JWKS unreachable at ${jwksUrl}: ${err instanceof Error ? err.message : String(err)}. JWT validation will allow all tokens \u2014 local dev fallback. Configure network access to the JWKS URL to enable real signature verification.`
72895
+ );
72896
+ const entry = {
72897
+ byKid: /* @__PURE__ */ new Map(),
72898
+ expiresAt: now() + failureTtlMs,
72899
+ passThrough: true
72900
+ };
72901
+ map.set(jwksUrl, entry);
72902
+ return entry;
72903
+ }
72904
+ },
72905
+ peek(jwksUrl) {
72906
+ return map.get(jwksUrl);
72907
+ },
72908
+ clear() {
72909
+ map.clear();
72910
+ }
72911
+ };
72912
+ }
72913
+ function buildCognitoJwksUrl(region, userPoolId) {
72914
+ return `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
72915
+ }
72916
+ function buildJwksUrlFromIssuer(issuer) {
72917
+ const stripped = issuer.replace(/\/+$/, "");
72918
+ return `${stripped}/.well-known/jwks.json`;
72919
+ }
72920
+ async function verifyCognitoJwt(authorizer, authorizationHeader, jwksCache, opts = {}) {
72921
+ const now = opts.now ?? (() => Date.now());
72922
+ const token = extractBearer(authorizationHeader);
72923
+ if (!token) {
72924
+ return { allow: false, identityHash: void 0, ttlSeconds: 0 };
72925
+ }
72926
+ const jwksUrl = buildCognitoJwksUrl(authorizer.region, authorizer.userPoolId);
72927
+ const expectedIssuer = `https://cognito-idp.${authorizer.region}.amazonaws.com/${authorizer.userPoolId}`;
72928
+ return verifyAndShape(token, jwksUrl, expectedIssuer, void 0, jwksCache, opts.warned, now);
72929
+ }
72930
+ async function verifyJwtAuthorizer(authorizer, authorizationHeader, jwksCache, opts = {}) {
72931
+ const now = opts.now ?? (() => Date.now());
72932
+ const token = extractBearer(authorizationHeader);
72933
+ if (!token) {
72934
+ return { allow: false, identityHash: void 0, ttlSeconds: 0 };
72935
+ }
72936
+ const jwksUrl = authorizer.region && authorizer.userPoolId ? buildCognitoJwksUrl(authorizer.region, authorizer.userPoolId) : buildJwksUrlFromIssuer(authorizer.issuer);
72937
+ return verifyAndShape(
72938
+ token,
72939
+ jwksUrl,
72940
+ authorizer.issuer.replace(/\/+$/, ""),
72941
+ authorizer.audience,
72942
+ jwksCache,
72943
+ opts.warned,
72944
+ now
72945
+ );
72946
+ }
72947
+ async function verifyAndShape(token, jwksUrl, expectedIssuer, expectedAudience, jwksCache, warned, now) {
72948
+ const identityHash = buildIdentityHash([token]);
72949
+ const jwks = await jwksCache.fetchAndCache(jwksUrl);
72950
+ if (jwks.passThrough) {
72951
+ if (warned && !warned.has(jwksUrl)) {
72952
+ warned.add(jwksUrl);
72953
+ getLogger().child("cognito-jwt").warn(
72954
+ `JWKS pass-through mode for ${jwksUrl}: token accepted without signature verification.`
72955
+ );
72956
+ }
72957
+ const parsed2 = parseJwt(token);
72958
+ if (parsed2) {
72959
+ return shapeAllowResult(parsed2, identityHash, now);
72960
+ }
72961
+ return {
72962
+ allow: true,
72963
+ principalId: "unknown",
72964
+ context: {},
72965
+ identityHash,
72966
+ ttlSeconds: 0
72967
+ };
72968
+ }
72969
+ const parsed = parseJwt(token);
72970
+ if (!parsed) {
72971
+ return { allow: false, identityHash, ttlSeconds: 0 };
72972
+ }
72973
+ const kid = parsed.header["kid"];
72974
+ if (typeof kid !== "string") {
72975
+ return { allow: false, identityHash, ttlSeconds: 0 };
72976
+ }
72977
+ const key = jwks.byKid.get(kid);
72978
+ if (!key) {
72979
+ return { allow: false, identityHash, ttlSeconds: 0 };
72980
+ }
72981
+ if (!verifyRs256(token, key)) {
72982
+ return { allow: false, identityHash, ttlSeconds: 0 };
72983
+ }
72984
+ const claims = parsed.payload;
72985
+ if (typeof claims["exp"] !== "number" || claims["exp"] * 1e3 <= now()) {
72986
+ return { allow: false, identityHash, ttlSeconds: 0 };
72987
+ }
72988
+ if (typeof claims["iss"] !== "string" || claims["iss"].replace(/\/+$/, "") !== expectedIssuer) {
72989
+ return { allow: false, identityHash, ttlSeconds: 0 };
72990
+ }
72991
+ if (expectedAudience && expectedAudience.length > 0) {
72992
+ const aud = claims["aud"];
72993
+ const clientId = claims["client_id"];
72994
+ const audValues = Array.isArray(aud) ? aud : aud !== void 0 ? [aud] : [];
72995
+ const matches = audValues.some((v) => typeof v === "string" && expectedAudience.includes(v)) || typeof clientId === "string" && expectedAudience.includes(clientId);
72996
+ if (!matches) {
72997
+ return { allow: false, identityHash, ttlSeconds: 0 };
72998
+ }
72999
+ }
73000
+ return shapeAllowResult(parsed, identityHash, now);
73001
+ }
73002
+ function shapeAllowResult(parsed, identityHash, now) {
73003
+ const claims = parsed.payload;
73004
+ const principalId = pickStringClaim(claims, "sub") ?? pickStringClaim(claims, "cognito:username") ?? pickStringClaim(claims, "username") ?? "unknown";
73005
+ const expMs = typeof claims["exp"] === "number" ? claims["exp"] * 1e3 : 0;
73006
+ const remainingSeconds = Math.max(0, Math.floor((expMs - now()) / 1e3));
73007
+ const ttlSeconds = Math.min(300, remainingSeconds);
73008
+ return {
73009
+ allow: true,
73010
+ principalId,
73011
+ context: claims,
73012
+ identityHash,
73013
+ ttlSeconds
73014
+ };
73015
+ }
73016
+ function pickStringClaim(claims, key) {
73017
+ const v = claims[key];
73018
+ return typeof v === "string" ? v : void 0;
73019
+ }
73020
+ function extractBearer(header) {
73021
+ if (!header)
73022
+ return void 0;
73023
+ const m = /^\s*Bearer\s+(.+)\s*$/i.exec(header);
73024
+ if (!m)
73025
+ return void 0;
73026
+ return m[1].trim();
73027
+ }
73028
+ function parseJwt(token) {
73029
+ const parts = token.split(".");
73030
+ if (parts.length !== 3)
73031
+ return void 0;
73032
+ try {
73033
+ const headerJson = base64UrlDecodeToString(parts[0]);
73034
+ const payloadJson = base64UrlDecodeToString(parts[1]);
73035
+ const header = JSON.parse(headerJson);
73036
+ const payload = JSON.parse(payloadJson);
73037
+ return {
73038
+ header,
73039
+ payload,
73040
+ signingInput: `${parts[0]}.${parts[1]}`,
73041
+ signatureB64: parts[2]
73042
+ };
73043
+ } catch {
73044
+ return void 0;
73045
+ }
73046
+ }
73047
+ function verifyRs256(token, key) {
73048
+ const parts = token.split(".");
73049
+ if (parts.length !== 3)
73050
+ return false;
73051
+ const signingInput = `${parts[0]}.${parts[1]}`;
73052
+ const signature = base64UrlDecodeToBuffer(parts[2]);
73053
+ try {
73054
+ const publicKey = createPublicKey({
73055
+ key: { kty: key.kty, n: key.n, e: key.e },
73056
+ format: "jwk"
73057
+ });
73058
+ const verifier = createVerify("RSA-SHA256");
73059
+ verifier.update(signingInput);
73060
+ verifier.end();
73061
+ return verifier.verify(publicKey, signature);
73062
+ } catch {
73063
+ return false;
73064
+ }
73065
+ }
73066
+ function base64UrlDecodeToString(input) {
73067
+ return base64UrlDecodeToBuffer(input).toString("utf-8");
73068
+ }
73069
+ function base64UrlDecodeToBuffer(input) {
73070
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/");
73071
+ const padding = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
73072
+ return Buffer.from(padded + padding, "base64");
73073
+ }
73074
+
73075
+ // src/local/http-server.ts
73076
+ async function startApiServer(opts) {
73077
+ const logger = getLogger().child("start-api");
73078
+ const server = createServer2((req, res) => {
73079
+ handleRequest(req, res, opts).catch((err) => {
73080
+ logger.error(
73081
+ `Unhandled request error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
73082
+ );
73083
+ if (!res.headersSent) {
73084
+ writeError(res, 502);
73085
+ }
73086
+ });
73087
+ });
73088
+ server.on("connection", (socket) => {
73089
+ socket.setNoDelay(true);
73090
+ });
73091
+ const { actualPort, actualHost } = await new Promise(
73092
+ (resolveListen, rejectListen) => {
73093
+ server.once("error", rejectListen);
73094
+ server.listen(opts.port, opts.host, () => {
73095
+ const addr = server.address();
73096
+ if (addr === null || typeof addr === "string") {
73097
+ rejectListen(new Error("Could not determine listening address"));
73098
+ return;
73099
+ }
73100
+ resolveListen({ actualPort: addr.port, actualHost: opts.host });
73101
+ });
73102
+ }
73103
+ );
73104
+ let closed = false;
73105
+ return {
73106
+ port: actualPort,
73107
+ host: actualHost,
73108
+ server,
73109
+ close: async () => {
73110
+ if (closed)
73111
+ return;
73112
+ closed = true;
73113
+ await new Promise((resolveClose) => {
73114
+ server.close(() => resolveClose());
73115
+ server.closeAllConnections?.();
73116
+ });
73117
+ }
73118
+ };
73119
+ }
73120
+ async function handleRequest(req, res, opts) {
73121
+ const logger = getLogger().child("start-api");
73122
+ const bodyBuf = await readBody(req);
73123
+ const rawUrl = req.url ?? "/";
73124
+ const method = (req.method ?? "GET").toUpperCase();
73125
+ const requestPath = rawUrl.split("?")[0] ?? "/";
73126
+ const flatRoutes = opts.routes.map((r) => r.route);
73127
+ const match = matchRoute(method, requestPath, flatRoutes);
73128
+ if (!match) {
73129
+ writeError(res, 404, '{"message":"Not Found"}');
73130
+ return;
73131
+ }
73132
+ const matchedEntry = opts.routes.find(
73133
+ (r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method
73134
+ );
73135
+ const authorizer = matchedEntry?.authorizer;
73136
+ const snapshot = {
73137
+ method,
73138
+ rawUrl,
73139
+ headers: collectHeaders(req),
73140
+ body: bodyBuf,
73141
+ ...req.socket.remoteAddress !== void 0 && { sourceIp: req.socket.remoteAddress }
73142
+ };
73143
+ const matchCtx = {
73144
+ route: match.route,
73145
+ pathParameters: match.pathParameters,
73146
+ matchedPath: requestPath
73147
+ };
73148
+ let baseEvent = match.route.apiVersion === "v1" ? buildRestV1Event(snapshot, matchCtx) : buildHttpApiV2Event(snapshot, matchCtx);
73149
+ let authResult;
73150
+ if (authorizer) {
73151
+ let outcome;
73152
+ try {
73153
+ outcome = await runAuthorizerPass(
73154
+ authorizer,
73155
+ snapshot,
73156
+ matchCtx,
73157
+ opts,
73158
+ baseEvent["requestContext"]
73159
+ );
73160
+ } catch (err) {
73161
+ logger.error(
73162
+ `Authorizer ${authorizer.logicalId} threw for ${match.route.declaredAt}: ${err instanceof Error ? err.message : String(err)}`
73163
+ );
73164
+ writeAuthRejection(res, match.route.apiVersion, "policy-deny");
73165
+ return;
73166
+ }
73167
+ if (!outcome.result.allow) {
73168
+ writeAuthRejection(res, match.route.apiVersion, outcome.denyKind ?? "policy-deny");
73169
+ return;
73170
+ }
73171
+ authResult = outcome.result;
73172
+ const overlay = buildOverlay(authorizer, authResult);
73173
+ if (overlay) {
73174
+ baseEvent = applyAuthorizerOverlay(baseEvent, overlay);
73175
+ }
73176
+ }
73177
+ let handle;
73178
+ try {
73179
+ handle = await opts.pool.acquire(match.route.lambdaLogicalId);
73180
+ } catch (err) {
73181
+ logger.error(
73182
+ `Failed to acquire container for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`
73183
+ );
73184
+ writeError(res, 502);
73185
+ return;
73186
+ }
73187
+ try {
73188
+ const invokeResult = await invokeRie(
73189
+ handle.containerHost,
73190
+ handle.hostPort,
73191
+ baseEvent,
73192
+ opts.rieTimeoutMs
73193
+ );
73194
+ const translated = translateLambdaResponse(invokeResult.payload, match.route.apiVersion);
73195
+ res.statusCode = translated.statusCode;
73196
+ for (const [name, value] of Object.entries(translated.headers)) {
73197
+ res.setHeader(name, value);
73198
+ }
73199
+ if (translated.cookies.length > 0) {
73200
+ res.setHeader("set-cookie", translated.cookies);
73201
+ }
73202
+ res.end(translated.body);
73203
+ } catch (err) {
73204
+ logger.error(
73205
+ `RIE invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`
73206
+ );
73207
+ if (!res.headersSent) {
73208
+ writeError(res, 502);
73209
+ } else {
73210
+ res.end();
73211
+ }
73212
+ } finally {
73213
+ opts.pool.release(handle);
73214
+ }
73215
+ }
73216
+ async function runAuthorizerPass(authorizer, snapshot, matchCtx, opts, requestContextV2) {
73217
+ const headers = lowercaseSingularHeaders(snapshot.headers);
73218
+ const queryStringParameters = parseQueryStringSingular(snapshot.rawUrl);
73219
+ const sourceIp = pickSourceIp(matchCtx.route.apiVersion, requestContextV2, snapshot);
73220
+ const reqSnap = {
73221
+ method: snapshot.method.toUpperCase(),
73222
+ headers,
73223
+ queryStringParameters,
73224
+ pathParameters: matchCtx.pathParameters,
73225
+ sourceIp,
73226
+ matchedPath: matchCtx.matchedPath,
73227
+ stage: matchCtx.route.stage
73228
+ };
73229
+ const methodArn = buildMethodArn({
73230
+ apiId: "local",
73231
+ accountId: "123456789012",
73232
+ stage: matchCtx.route.stage,
73233
+ method: snapshot.method,
73234
+ path: matchCtx.matchedPath
73235
+ });
73236
+ const cache2 = opts.authorizerCache;
73237
+ if (authorizer.kind === "lambda-token") {
73238
+ const token = headers[authorizer.tokenHeader];
73239
+ if (!token) {
73240
+ return { result: { allow: false }, denyKind: "missing-identity" };
73241
+ }
73242
+ if (cache2) {
73243
+ const cached = cache2.get(authorizer.logicalId, hashOne(token));
73244
+ if (cached) {
73245
+ if (cached.policy !== void 0) {
73246
+ return shapeOutcome(evaluateCachedLambdaPolicy(cached, methodArn));
73247
+ }
73248
+ return shapeOutcome(cached);
73249
+ }
73250
+ }
73251
+ const result2 = await invokeTokenAuthorizer(authorizer, reqSnap, {
73252
+ pool: opts.pool,
73253
+ rieTimeoutMs: opts.rieTimeoutMs,
73254
+ methodArn,
73255
+ mockAccountId: "123456789012",
73256
+ mockApiId: "local"
73257
+ });
73258
+ if (cache2 && result2.identityHash !== void 0) {
73259
+ cache2.set(
73260
+ authorizer.logicalId,
73261
+ result2.identityHash,
73262
+ authorizer.resultTtlSeconds,
73263
+ stripHash(result2)
73264
+ );
73265
+ }
73266
+ return shapeOutcome(stripHash(result2));
73267
+ }
73268
+ if (authorizer.kind === "lambda-request") {
73269
+ const { identityHash, missing } = computeRequestIdentityHash(authorizer, reqSnap);
73270
+ if (missing) {
73271
+ return { result: { allow: false }, denyKind: "missing-identity" };
73272
+ }
73273
+ if (cache2 && authorizer.resultTtlSeconds > 0) {
73274
+ const cached = cache2.get(authorizer.logicalId, identityHash);
73275
+ if (cached) {
73276
+ if (cached.policy !== void 0) {
73277
+ return shapeOutcome(evaluateCachedLambdaPolicy(cached, methodArn));
73278
+ }
73279
+ return shapeOutcome(cached);
73280
+ }
73281
+ }
73282
+ const result2 = await invokeRequestAuthorizer(authorizer, reqSnap, {
73283
+ pool: opts.pool,
73284
+ rieTimeoutMs: opts.rieTimeoutMs,
73285
+ methodArn,
73286
+ mockAccountId: "123456789012",
73287
+ mockApiId: "local"
73288
+ });
73289
+ if (cache2 && result2.identityHash !== void 0 && authorizer.resultTtlSeconds > 0) {
73290
+ cache2.set(
73291
+ authorizer.logicalId,
73292
+ result2.identityHash,
73293
+ authorizer.resultTtlSeconds,
73294
+ stripHash(result2)
73295
+ );
73296
+ }
73297
+ return shapeOutcome(stripHash(result2));
73298
+ }
73299
+ if (!opts.jwksCache) {
73300
+ return { result: { allow: false }, denyKind: "policy-deny" };
73301
+ }
73302
+ const authHeader = headers["authorization"];
73303
+ const jwksOpts = { ...opts.jwksWarnedUrls && { warned: opts.jwksWarnedUrls } };
73304
+ if (authorizer.kind === "cognito") {
73305
+ if (cache2 && authHeader !== void 0) {
73306
+ const cached = cache2.get(authorizer.logicalId, hashOne(authHeader));
73307
+ if (cached)
73308
+ return shapeOutcome(cached);
73309
+ }
73310
+ const result2 = await verifyCognitoJwt(authorizer, authHeader, opts.jwksCache, jwksOpts);
73311
+ if (cache2 && result2.identityHash !== void 0 && result2.ttlSeconds > 0) {
73312
+ cache2.set(
73313
+ authorizer.logicalId,
73314
+ result2.identityHash,
73315
+ result2.ttlSeconds,
73316
+ stripHashAndTtl(result2)
73317
+ );
73318
+ }
73319
+ if (!result2.allow && authHeader === void 0) {
73320
+ return { result: stripHashAndTtl(result2), denyKind: "missing-identity" };
73321
+ }
73322
+ return shapeOutcome(stripHashAndTtl(result2));
73323
+ }
73324
+ if (cache2 && authHeader !== void 0) {
73325
+ const cached = cache2.get(authorizer.logicalId, hashOne(authHeader));
73326
+ if (cached)
73327
+ return shapeOutcome(cached);
73328
+ }
73329
+ const result = await verifyJwtAuthorizer(authorizer, authHeader, opts.jwksCache, jwksOpts);
73330
+ if (cache2 && result.identityHash !== void 0 && result.ttlSeconds > 0) {
73331
+ cache2.set(
73332
+ authorizer.logicalId,
73333
+ result.identityHash,
73334
+ result.ttlSeconds,
73335
+ stripHashAndTtl(result)
73336
+ );
73337
+ }
73338
+ if (!result.allow && authHeader === void 0) {
73339
+ return { result: stripHashAndTtl(result), denyKind: "missing-identity" };
73340
+ }
73341
+ return shapeOutcome(stripHashAndTtl(result));
73342
+ }
73343
+ function shapeOutcome(result) {
73344
+ if (result.allow)
73345
+ return { result };
73346
+ return { result, denyKind: "policy-deny" };
73347
+ }
73348
+ function pickSourceIp(apiVersion, requestContext, snapshot) {
73349
+ if (apiVersion === "v1") {
73350
+ const identity = requestContext["identity"];
73351
+ if (identity && typeof identity === "object" && !Array.isArray(identity) && typeof identity["sourceIp"] === "string") {
73352
+ return identity["sourceIp"];
73353
+ }
73354
+ } else {
73355
+ const http = requestContext["http"];
73356
+ if (http && typeof http === "object" && !Array.isArray(http) && typeof http["sourceIp"] === "string") {
73357
+ return http["sourceIp"];
73358
+ }
73359
+ }
73360
+ return snapshot.sourceIp ?? "127.0.0.1";
73361
+ }
73362
+ function buildOverlay(authorizer, result) {
73363
+ if (authorizer.kind === "lambda-token" || authorizer.kind === "lambda-request") {
73364
+ const isV2 = authorizer.kind === "lambda-request" && authorizer.apiVersion === "v2";
73365
+ return isV2 ? {
73366
+ kind: "lambda-http-v2",
73367
+ ...result.principalId !== void 0 && { principalId: result.principalId },
73368
+ ...result.context && { context: result.context }
73369
+ } : {
73370
+ kind: "lambda-rest-v1",
73371
+ ...result.principalId !== void 0 && { principalId: result.principalId },
73372
+ ...result.context && { context: result.context }
73373
+ };
73374
+ }
73375
+ if (authorizer.kind === "cognito") {
73376
+ return { kind: "cognito-rest-v1", claims: result.context ?? {} };
73377
+ }
73378
+ return { kind: "jwt-http-v2", claims: result.context ?? {} };
73379
+ }
73380
+ function writeAuthRejection(res, apiVersion, denyKind) {
73381
+ if (apiVersion === "v2") {
73382
+ writeError(res, 401, '{"message":"Unauthorized"}');
73383
+ return;
73384
+ }
73385
+ if (denyKind === "missing-identity") {
73386
+ writeError(res, 401, '{"message":"Unauthorized"}');
73387
+ return;
73388
+ }
73389
+ writeError(res, 403, '{"message":"Forbidden"}');
73390
+ }
73391
+ function hashOne(value) {
73392
+ return value;
73393
+ }
73394
+ function stripHash(r) {
73395
+ const { identityHash, ...rest } = r;
73396
+ return rest;
73397
+ }
73398
+ function stripHashAndTtl(r) {
73399
+ const { identityHash, ttlSeconds, ...rest } = r;
73400
+ return rest;
73401
+ }
73402
+ function lowercaseSingularHeaders(raw) {
73403
+ const out = {};
73404
+ for (const [name, values] of Object.entries(raw)) {
73405
+ out[name.toLowerCase()] = values.join(",");
73406
+ }
73407
+ return out;
73408
+ }
73409
+ function parseQueryStringSingular(rawUrl) {
73410
+ const q = rawUrl.indexOf("?");
73411
+ if (q < 0)
73412
+ return {};
73413
+ const raw = rawUrl.slice(q + 1);
73414
+ if (raw.length === 0)
73415
+ return {};
73416
+ const out = {};
73417
+ for (const pair of raw.split("&")) {
73418
+ if (pair.length === 0)
73419
+ continue;
73420
+ const eq = pair.indexOf("=");
73421
+ const rawKey = eq === -1 ? pair : pair.slice(0, eq);
73422
+ const rawValue = eq === -1 ? "" : pair.slice(eq + 1);
73423
+ let key = rawKey;
73424
+ let value = rawValue;
73425
+ try {
73426
+ key = decodeURIComponent(rawKey);
73427
+ } catch {
73428
+ }
73429
+ try {
73430
+ value = decodeURIComponent(rawValue);
73431
+ } catch {
73432
+ }
73433
+ out[key] = value;
73434
+ }
73435
+ return out;
73436
+ }
73437
+ function readBody(req) {
73438
+ return new Promise((resolveBody, rejectBody) => {
73439
+ const chunks = [];
73440
+ req.on("data", (chunk) => {
73441
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
73442
+ });
73443
+ req.on("end", () => resolveBody(Buffer.concat(chunks)));
73444
+ req.on("error", rejectBody);
73445
+ });
73446
+ }
73447
+ function collectHeaders(req) {
73448
+ const out = {};
73449
+ for (const [name, value] of Object.entries(req.headers)) {
73450
+ if (Array.isArray(value)) {
73451
+ out[name] = value;
73452
+ } else if (typeof value === "string") {
73453
+ out[name] = [value];
73454
+ }
73455
+ }
73456
+ return out;
73457
+ }
73458
+ function writeError(res, statusCode, body = '{"message":"Internal server error"}') {
73459
+ res.statusCode = statusCode;
73460
+ res.setHeader("content-type", "application/json");
73461
+ res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
73462
+ res.end(body);
73463
+ }
73464
+
73465
+ // src/local/authorizer-cache.ts
73466
+ function createAuthorizerCache(opts = {}) {
73467
+ const now = opts.now ?? (() => Date.now());
73468
+ const map = /* @__PURE__ */ new Map();
73469
+ const buildKey = (auth, identity) => `${auth}\0${identity}`;
73470
+ const sweep = () => {
73471
+ const t = now();
73472
+ for (const [k, v] of map) {
73473
+ if (v.expiresAt <= t)
73474
+ map.delete(k);
73475
+ }
73476
+ };
73477
+ return {
73478
+ get(authorizerLogicalId, identityHash) {
73479
+ const key = buildKey(authorizerLogicalId, identityHash);
73480
+ const entry = map.get(key);
73481
+ if (!entry)
73482
+ return void 0;
73483
+ if (entry.expiresAt <= now()) {
73484
+ map.delete(key);
73485
+ return void 0;
73486
+ }
73487
+ return entry.result;
73488
+ },
73489
+ set(authorizerLogicalId, identityHash, ttlSeconds, result) {
73490
+ if (ttlSeconds <= 0)
73491
+ return;
73492
+ const key = buildKey(authorizerLogicalId, identityHash);
73493
+ map.set(key, { expiresAt: now() + ttlSeconds * 1e3, result });
73494
+ },
73495
+ clear() {
73496
+ map.clear();
73497
+ },
73498
+ size() {
73499
+ sweep();
73500
+ return map.size;
73501
+ }
73502
+ };
73503
+ }
73504
+
73505
+ // src/cli/commands/local-start-api.ts
73506
+ async function localStartApiCommand(options) {
73507
+ const logger = getLogger();
73508
+ if (options.verbose) {
73509
+ logger.setLevel("debug");
73510
+ }
73511
+ warnIfDeprecatedRegion(options);
73512
+ await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
73513
+ await ensureDockerAvailable();
73514
+ const appCmd = resolveApp(options.app);
73515
+ if (!appCmd) {
73516
+ throw new Error('No CDK app specified. Pass --app, set CDKD_APP, or add "app" to cdk.json.');
72295
73517
  }
72296
73518
  logger.info("Synthesizing CDK app...");
72297
73519
  const synthesizer = new Synthesizer();
@@ -72314,11 +73536,13 @@ async function localStartApiCommand(options) {
72314
73536
  "No supported API routes were discovered. cdkd local start-api supports AWS::ApiGateway::* (REST v1), AWS::ApiGatewayV2::* (HTTP), and AWS::Lambda::Url (Function URL) with AWS_PROXY integrations only."
72315
73537
  );
72316
73538
  }
72317
- const lambdaIds = uniqueLambdaIds(routes);
73539
+ const routesWithAuth = attachAuthorizers(targetStacks, routes);
73540
+ const lambdaIds = uniqueLambdaIds(routes, routesWithAuth);
72318
73541
  const overrides = readEnvOverridesFile(options.envVars);
72319
73542
  const debugPortBase = options.debugPortBase ? parseDebugPort(options.debugPortBase) : void 0;
72320
73543
  const specs = /* @__PURE__ */ new Map();
72321
73544
  const inlineTmpDirs = /* @__PURE__ */ new Set();
73545
+ const layerTmpDirs = /* @__PURE__ */ new Set();
72322
73546
  for (let i = 0; i < lambdaIds.length; i++) {
72323
73547
  const logicalId = lambdaIds[i];
72324
73548
  const spec = await buildContainerSpec({
@@ -72329,7 +73553,8 @@ async function localStartApiCommand(options) {
72329
73553
  containerHost: options.containerHost,
72330
73554
  ...debugPortBase !== void 0 && { debugPort: debugPortBase + i },
72331
73555
  stsRegion: options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"],
72332
- inlineTmpDirs
73556
+ inlineTmpDirs,
73557
+ layerTmpDirs
72333
73558
  });
72334
73559
  specs.set(logicalId, spec);
72335
73560
  }
@@ -72368,12 +73593,20 @@ async function localStartApiCommand(options) {
72368
73593
  if (!Number.isFinite(port) || port < 0 || port > 65535) {
72369
73594
  throw new Error(`--port must be 0..65535 (got ${options.port}).`);
72370
73595
  }
73596
+ const authorizerCache = createAuthorizerCache();
73597
+ const jwksCache = createJwksCache();
73598
+ const jwksWarnedUrls = /* @__PURE__ */ new Set();
73599
+ await prewarmJwks(routesWithAuth, jwksCache);
73600
+ warnVpcConfigLambdas(routesWithAuth, targetStacks);
72371
73601
  const server = await startApiServer({
72372
- routes,
73602
+ routes: routesWithAuth,
72373
73603
  pool,
72374
73604
  rieTimeoutMs,
72375
73605
  host: options.host,
72376
- port
73606
+ port,
73607
+ authorizerCache,
73608
+ jwksCache,
73609
+ jwksWarnedUrls
72377
73610
  });
72378
73611
  printRouteTable(routes);
72379
73612
  logger.info(
@@ -72416,6 +73649,15 @@ async function localStartApiCommand(options) {
72416
73649
  );
72417
73650
  }
72418
73651
  }
73652
+ for (const dir of layerTmpDirs) {
73653
+ try {
73654
+ rmSync2(dir, { recursive: true, force: true });
73655
+ } catch (err) {
73656
+ logger.warn(
73657
+ `Failed to remove merged-layers tmpdir ${dir}: ${err instanceof Error ? err.message : String(err)}`
73658
+ );
73659
+ }
73660
+ }
72419
73661
  process.exit(exitCode);
72420
73662
  };
72421
73663
  process.on("SIGINT", () => {
@@ -72450,7 +73692,7 @@ function pickTargetStacks(stacks, pattern) {
72450
73692
  `Multi-stack app: pass --stack <name> to pick a target. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`
72451
73693
  );
72452
73694
  }
72453
- function uniqueLambdaIds(routes) {
73695
+ function uniqueLambdaIds(routes, routesWithAuth) {
72454
73696
  const seen = /* @__PURE__ */ new Set();
72455
73697
  const out = [];
72456
73698
  for (const r of routes) {
@@ -72459,8 +73701,67 @@ function uniqueLambdaIds(routes) {
72459
73701
  out.push(r.lambdaLogicalId);
72460
73702
  }
72461
73703
  }
73704
+ for (const entry of routesWithAuth) {
73705
+ const auth = entry.authorizer;
73706
+ if (!auth)
73707
+ continue;
73708
+ if (auth.kind === "lambda-token" || auth.kind === "lambda-request") {
73709
+ if (!seen.has(auth.lambdaLogicalId)) {
73710
+ seen.add(auth.lambdaLogicalId);
73711
+ out.push(auth.lambdaLogicalId);
73712
+ }
73713
+ }
73714
+ }
72462
73715
  return out;
72463
73716
  }
73717
+ async function prewarmJwks(routesWithAuth, jwksCache) {
73718
+ const urls = /* @__PURE__ */ new Set();
73719
+ for (const entry of routesWithAuth) {
73720
+ const auth = entry.authorizer;
73721
+ if (!auth)
73722
+ continue;
73723
+ if (auth.kind === "cognito") {
73724
+ urls.add(buildCognitoJwksUrl(auth.region, auth.userPoolId));
73725
+ } else if (auth.kind === "jwt") {
73726
+ const url = auth.region && auth.userPoolId ? buildCognitoJwksUrl(auth.region, auth.userPoolId) : buildJwksUrlFromIssuer(auth.issuer);
73727
+ urls.add(url);
73728
+ }
73729
+ }
73730
+ await Promise.all([...urls].map((u) => jwksCache.fetchAndCache(u)));
73731
+ }
73732
+ function warnVpcConfigLambdas(routesWithAuth, stacks) {
73733
+ const logger = getLogger();
73734
+ const seen = /* @__PURE__ */ new Set();
73735
+ const reachable = [];
73736
+ for (const entry of routesWithAuth) {
73737
+ if (!seen.has(entry.route.lambdaLogicalId)) {
73738
+ seen.add(entry.route.lambdaLogicalId);
73739
+ reachable.push(entry.route.lambdaLogicalId);
73740
+ }
73741
+ const auth = entry.authorizer;
73742
+ if (auth && (auth.kind === "lambda-token" || auth.kind === "lambda-request")) {
73743
+ if (!seen.has(auth.lambdaLogicalId)) {
73744
+ seen.add(auth.lambdaLogicalId);
73745
+ reachable.push(auth.lambdaLogicalId);
73746
+ }
73747
+ }
73748
+ }
73749
+ for (const logicalId of reachable) {
73750
+ for (const stack of stacks) {
73751
+ const resource = stack.template.Resources?.[logicalId];
73752
+ if (!resource || resource.Type !== "AWS::Lambda::Function")
73753
+ continue;
73754
+ const props = resource.Properties ?? {};
73755
+ const vpcConfig = props["VpcConfig"];
73756
+ if (vpcConfig && typeof vpcConfig === "object" && Object.keys(vpcConfig).length > 0) {
73757
+ logger.warn(
73758
+ `Lambda ${logicalId} has VpcConfig \u2014 local container will reach external services via the host's network, NOT through the deployed VPC's NAT/private subnets. Calls to private RDS/ElastiCache will fail. See docs/cli-reference.md (cdkd local start-api \u2014 Limitations) for details.`
73759
+ );
73760
+ }
73761
+ break;
73762
+ }
73763
+ }
73764
+ }
72464
73765
  async function buildContainerSpec(args) {
72465
73766
  const {
72466
73767
  logicalId,
@@ -72470,7 +73771,8 @@ async function buildContainerSpec(args) {
72470
73771
  containerHost,
72471
73772
  debugPort,
72472
73773
  stsRegion,
72473
- inlineTmpDirs
73774
+ inlineTmpDirs,
73775
+ layerTmpDirs
72474
73776
  } = args;
72475
73777
  const lambda = resolveLambdaByLogicalId(logicalId, stacks);
72476
73778
  const codeDir = lambda.codePath ?? materializeInlineCode(
@@ -72479,6 +73781,7 @@ async function buildContainerSpec(args) {
72479
73781
  resolveRuntimeFileExtension(lambda.runtime),
72480
73782
  inlineTmpDirs
72481
73783
  );
73784
+ const optDir = materializeLambdaLayers(lambda.layers, layerTmpDirs);
72482
73785
  const templateEnv = getTemplateEnv(lambda.resource);
72483
73786
  const envResult = resolveEnvVars(logicalId, templateEnv, overrides);
72484
73787
  for (const key of envResult.unresolved) {
@@ -72514,10 +73817,23 @@ async function buildContainerSpec(args) {
72514
73817
  codeDir,
72515
73818
  env: dockerEnv,
72516
73819
  containerHost,
73820
+ ...optDir !== void 0 && { optDir },
72517
73821
  ...debugPort !== void 0 && { debugPort }
72518
73822
  };
72519
73823
  return spec;
72520
73824
  }
73825
+ function materializeLambdaLayers(layers, layerTmpDirs) {
73826
+ if (layers.length === 0)
73827
+ return void 0;
73828
+ if (layers.length === 1)
73829
+ return layers[0].assetPath;
73830
+ const dir = mkdtempSync2(path.join(tmpdir2(), "cdkd-local-start-api-layers-"));
73831
+ for (const layer of layers) {
73832
+ cpSync(layer.assetPath, dir, { recursive: true, force: true });
73833
+ }
73834
+ layerTmpDirs.add(dir);
73835
+ return dir;
73836
+ }
72521
73837
  function resolveLambdaByLogicalId(logicalId, stacks) {
72522
73838
  for (const stack of stacks) {
72523
73839
  const resource = stack.template.Resources?.[logicalId];
@@ -72548,6 +73864,7 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
72548
73864
  if (!inlineCode) {
72549
73865
  codePath = resolveAssetCodePath2(stack, logicalId, resource);
72550
73866
  }
73867
+ const layers = resolveLambdaLayers(stack, logicalId, props);
72551
73868
  return {
72552
73869
  kind: "zip",
72553
73870
  stack,
@@ -72558,6 +73875,7 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
72558
73875
  memoryMb,
72559
73876
  timeoutSec,
72560
73877
  codePath,
73878
+ layers,
72561
73879
  ...inlineCode !== void 0 && { inlineCode }
72562
73880
  };
72563
73881
  }
@@ -72701,7 +74019,7 @@ function parseDebugPort(raw) {
72701
74019
  }
72702
74020
  function createLocalStartApiCommand() {
72703
74021
  const startApi = new Command14("start-api").description(
72704
- "Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required)."
74022
+ "Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers and Cognito User Pool / HTTP v2 JWT authorizers; when JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line \u2014 local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail."
72705
74023
  ).addOption(
72706
74024
  new Option7("--port <port>", "HTTP server port (default: auto-allocate)").default("0")
72707
74025
  ).addOption(new Option7("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option7("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(
@@ -72744,123 +74062,170 @@ async function localInvokeCommand(target, options) {
72744
74062
  logger.setLevel("debug");
72745
74063
  }
72746
74064
  warnIfDeprecatedRegion(options);
72747
- await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
72748
- await ensureDockerAvailable();
72749
- const appCmd = resolveApp(options.app);
72750
- if (!appCmd) {
72751
- throw new Error('No CDK app specified. Pass --app, set CDKD_APP, or add "app" to cdk.json.');
72752
- }
72753
- logger.info("Synthesizing CDK app...");
72754
- const synthesizer = new Synthesizer();
72755
- const context = parseContextOptions(options.context);
72756
- const synthOpts = {
72757
- app: appCmd,
72758
- output: options.output,
72759
- ...options.region && { region: options.region },
72760
- ...options.profile && { profile: options.profile },
72761
- ...Object.keys(context).length > 0 && { context }
72762
- };
72763
- const { stacks } = await synthesizer.synthesize(synthOpts);
72764
- const lambda = resolveLambdaTarget(target, stacks);
72765
- const targetLabel = lambda.kind === "zip" ? lambda.runtime : "container image";
72766
- logger.info(`Target: ${lambda.stack.stackName}/${lambda.logicalId} (${targetLabel})`);
72767
- const imagePlan = await resolveImagePlan(lambda, options);
72768
- let stateAudit;
72769
- let templateEnv = getTemplateEnv2(lambda.resource);
72770
- let stateForRoleHint;
72771
- if (options.fromState) {
72772
- const loaded = await loadStateForStack(lambda.stack.stackName, lambda.stack.region, {
72773
- ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
72774
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
72775
- statePrefix: options.statePrefix,
72776
- ...options.region !== void 0 && { region: options.region },
72777
- ...options.profile !== void 0 && { profile: options.profile }
72778
- });
72779
- if (loaded) {
72780
- stateForRoleHint = loaded.state;
72781
- const { env, audit } = substituteEnvVarsFromState(templateEnv, loaded.state.resources);
72782
- templateEnv = env;
72783
- stateAudit = audit;
72784
- for (const key of audit.resolvedKeys) {
72785
- logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
72786
- }
72787
- for (const { key, reason } of audit.unresolved) {
72788
- logger.warn(
72789
- `--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`
74065
+ let imagePlan;
74066
+ let containerId;
74067
+ let stopLogs;
74068
+ let sigintHandler;
74069
+ const cleanup = async () => {
74070
+ if (stopLogs) {
74071
+ try {
74072
+ stopLogs();
74073
+ } catch (err) {
74074
+ getLogger().debug(
74075
+ `streamLogs stop failed: ${err instanceof Error ? err.message : String(err)}`
74076
+ );
74077
+ }
74078
+ }
74079
+ if (containerId) {
74080
+ try {
74081
+ await removeContainer(containerId);
74082
+ } catch (err) {
74083
+ getLogger().debug(
74084
+ `removeContainer(${containerId}) failed: ${err instanceof Error ? err.message : String(err)}`
74085
+ );
74086
+ }
74087
+ }
74088
+ if (imagePlan?.inlineTmpDir) {
74089
+ try {
74090
+ rmSync3(imagePlan.inlineTmpDir, { recursive: true, force: true });
74091
+ } catch (err) {
74092
+ getLogger().debug(
74093
+ `Failed to remove inline-code tmpdir ${imagePlan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`
74094
+ );
74095
+ }
74096
+ }
74097
+ if (imagePlan?.layersTmpDir) {
74098
+ try {
74099
+ rmSync3(imagePlan.layersTmpDir, { recursive: true, force: true });
74100
+ } catch (err) {
74101
+ getLogger().debug(
74102
+ `Failed to remove merged-layers tmpdir ${imagePlan.layersTmpDir}: ${err instanceof Error ? err.message : String(err)}`
72790
74103
  );
72791
74104
  }
72792
74105
  }
72793
- }
72794
- const overrides = readEnvOverridesFile2(options.envVars);
72795
- const envResult = resolveEnvVars(lambda.logicalId, templateEnv, overrides);
72796
- for (const key of envResult.unresolved) {
72797
- if (stateAudit && stateAudit.unresolved.some((u) => u.key === key))
72798
- continue;
72799
- logger.warn(
72800
- `Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${lambda.logicalId}":{"${key}":"<literal>"}}) or pass --from-state to recover deployed values.`
72801
- );
72802
- }
72803
- if (options.fromState && !options.assumeRole && stateForRoleHint) {
72804
- suggestAssumeRoleFromState(stateForRoleHint, lambda.logicalId);
72805
- }
72806
- const event = await readEvent(options);
72807
- const dockerEnv = {
72808
- AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
72809
- AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
72810
- AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
72811
- AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
72812
- AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${lambda.logicalId}`,
72813
- AWS_LAMBDA_LOG_STREAM_NAME: "local",
72814
- ...envResult.resolved
72815
74106
  };
72816
- if (options.assumeRole) {
72817
- const stsRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
72818
- const creds = await assumeLambdaExecutionRole2(options.assumeRole, stsRegion);
72819
- dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
72820
- dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
72821
- dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
72822
- if (stsRegion)
72823
- dockerEnv["AWS_REGION"] = stsRegion;
72824
- } else {
72825
- forwardAwsEnv2(dockerEnv);
72826
- }
72827
- let debugPort;
72828
- if (options.debugPort) {
72829
- debugPort = Number(options.debugPort);
72830
- if (!Number.isInteger(debugPort) || debugPort <= 0 || debugPort > 65535) {
72831
- throw new Error(`--debug-port must be an integer in 1..65535, got '${options.debugPort}'`);
74107
+ try {
74108
+ await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
74109
+ await ensureDockerAvailable();
74110
+ const appCmd = resolveApp(options.app);
74111
+ if (!appCmd) {
74112
+ throw new Error('No CDK app specified. Pass --app, set CDKD_APP, or add "app" to cdk.json.');
72832
74113
  }
72833
- dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
72834
- if (lambda.kind === "image") {
74114
+ logger.info("Synthesizing CDK app...");
74115
+ const synthesizer = new Synthesizer();
74116
+ const context = parseContextOptions(options.context);
74117
+ const synthOpts = {
74118
+ app: appCmd,
74119
+ output: options.output,
74120
+ ...options.region && { region: options.region },
74121
+ ...options.profile && { profile: options.profile },
74122
+ ...Object.keys(context).length > 0 && { context }
74123
+ };
74124
+ const { stacks } = await synthesizer.synthesize(synthOpts);
74125
+ const lambda = resolveLambdaTarget(target, stacks);
74126
+ const targetLabel = lambda.kind === "zip" ? lambda.runtime : "container image";
74127
+ logger.info(`Target: ${lambda.stack.stackName}/${lambda.logicalId} (${targetLabel})`);
74128
+ imagePlan = await resolveImagePlan(lambda, options);
74129
+ let stateAudit;
74130
+ let templateEnv = getTemplateEnv2(lambda.resource);
74131
+ let stateForRoleHint;
74132
+ if (options.fromState) {
74133
+ const loaded = await loadStateForStack(lambda.stack.stackName, lambda.stack.region, {
74134
+ ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
74135
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
74136
+ statePrefix: options.statePrefix,
74137
+ ...options.region !== void 0 && { region: options.region },
74138
+ ...options.profile !== void 0 && { profile: options.profile }
74139
+ });
74140
+ if (loaded) {
74141
+ stateForRoleHint = loaded.state;
74142
+ const { env, audit } = substituteEnvVarsFromState(templateEnv, loaded.state.resources);
74143
+ templateEnv = env;
74144
+ stateAudit = audit;
74145
+ for (const key of audit.resolvedKeys) {
74146
+ logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
74147
+ }
74148
+ for (const { key, reason } of audit.unresolved) {
74149
+ logger.warn(
74150
+ `--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`
74151
+ );
74152
+ }
74153
+ }
74154
+ }
74155
+ const overrides = readEnvOverridesFile2(options.envVars);
74156
+ const envResult = resolveEnvVars(lambda.logicalId, templateEnv, overrides);
74157
+ for (const key of envResult.unresolved) {
74158
+ if (stateAudit && stateAudit.unresolved.some((u) => u.key === key))
74159
+ continue;
72835
74160
  logger.warn(
72836
- "--debug-port sets NODE_OPTIONS unconditionally on container Lambdas. If the image's runtime is not Node.js, this flag is a no-op."
74161
+ `Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${lambda.logicalId}":{"${key}":"<literal>"}}) or pass --from-state to recover deployed values.`
72837
74162
  );
72838
74163
  }
72839
- }
72840
- const hostPort = await pickFreePort();
72841
- const containerHost = options.containerHost;
72842
- logger.info(`Starting container (image=${imagePlan.image}, port=${hostPort})...`);
72843
- const containerId = await runDetached({
72844
- image: imagePlan.image,
72845
- mounts: imagePlan.mounts,
72846
- env: dockerEnv,
72847
- cmd: imagePlan.cmd,
72848
- hostPort,
72849
- host: containerHost,
72850
- ...debugPort !== void 0 && { debugPort },
72851
- ...imagePlan.platform !== void 0 && { platform: imagePlan.platform },
72852
- ...imagePlan.entryPoint !== void 0 && { entryPoint: imagePlan.entryPoint },
72853
- ...imagePlan.workingDir !== void 0 && { workingDir: imagePlan.workingDir }
72854
- });
72855
- const stopLogs = streamLogs(containerId);
72856
- const sigintHandler = () => {
72857
- stopLogs();
72858
- void removeContainer(containerId).then(() => {
72859
- process.exit(130);
72860
- });
72861
- };
72862
- process.on("SIGINT", sigintHandler);
72863
- try {
74164
+ if (options.fromState && !options.assumeRole && stateForRoleHint) {
74165
+ suggestAssumeRoleFromState(stateForRoleHint, lambda.logicalId);
74166
+ }
74167
+ const event = await readEvent(options);
74168
+ const dockerEnv = {
74169
+ AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
74170
+ AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
74171
+ AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
74172
+ AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
74173
+ AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${lambda.logicalId}`,
74174
+ AWS_LAMBDA_LOG_STREAM_NAME: "local",
74175
+ ...envResult.resolved
74176
+ };
74177
+ if (options.assumeRole) {
74178
+ const stsRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
74179
+ const creds = await assumeLambdaExecutionRole2(options.assumeRole, stsRegion);
74180
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
74181
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
74182
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
74183
+ if (stsRegion)
74184
+ dockerEnv["AWS_REGION"] = stsRegion;
74185
+ } else {
74186
+ forwardAwsEnv2(dockerEnv);
74187
+ }
74188
+ let debugPort;
74189
+ if (options.debugPort) {
74190
+ debugPort = Number(options.debugPort);
74191
+ if (!Number.isInteger(debugPort) || debugPort <= 0 || debugPort > 65535) {
74192
+ throw new Error(`--debug-port must be an integer in 1..65535, got '${options.debugPort}'`);
74193
+ }
74194
+ dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
74195
+ if (lambda.kind === "image") {
74196
+ logger.warn(
74197
+ "--debug-port sets NODE_OPTIONS unconditionally on container Lambdas. If the image's runtime is not Node.js, this flag is a no-op."
74198
+ );
74199
+ }
74200
+ }
74201
+ const hostPort = await pickFreePort();
74202
+ const containerHost = options.containerHost;
74203
+ if (lambda.layers.length > 0) {
74204
+ logger.info(
74205
+ `Mounting ${lambda.layers.length} Lambda layer${lambda.layers.length === 1 ? "" : "s"} at /opt`
74206
+ );
74207
+ }
74208
+ logger.info(`Starting container (image=${imagePlan.image}, port=${hostPort})...`);
74209
+ containerId = await runDetached({
74210
+ image: imagePlan.image,
74211
+ mounts: imagePlan.mounts,
74212
+ extraMounts: imagePlan.extraMounts,
74213
+ env: dockerEnv,
74214
+ cmd: imagePlan.cmd,
74215
+ hostPort,
74216
+ host: containerHost,
74217
+ ...debugPort !== void 0 && { debugPort },
74218
+ ...imagePlan.platform !== void 0 && { platform: imagePlan.platform },
74219
+ ...imagePlan.entryPoint !== void 0 && { entryPoint: imagePlan.entryPoint },
74220
+ ...imagePlan.workingDir !== void 0 && { workingDir: imagePlan.workingDir }
74221
+ });
74222
+ stopLogs = streamLogs(containerId);
74223
+ sigintHandler = () => {
74224
+ void cleanup().then(() => {
74225
+ process.exit(130);
74226
+ });
74227
+ };
74228
+ process.on("SIGINT", sigintHandler);
72864
74229
  await waitForRieReady(containerHost, hostPort, 5e3);
72865
74230
  const invokeTimeoutMs = Math.max(3e4, lambda.timeoutSec * 2 * 1e3);
72866
74231
  const result = await invokeRie(containerHost, hostPort, event, invokeTimeoutMs);
@@ -72868,18 +74233,9 @@ async function localInvokeCommand(target, options) {
72868
74233
  process.stdout.write(`${result.raw}
72869
74234
  `);
72870
74235
  } finally {
72871
- process.off("SIGINT", sigintHandler);
72872
- stopLogs();
72873
- await removeContainer(containerId);
72874
- if (imagePlan.inlineTmpDir) {
72875
- try {
72876
- rmSync3(imagePlan.inlineTmpDir, { recursive: true, force: true });
72877
- } catch (err) {
72878
- getLogger().debug(
72879
- `Failed to remove inline-code tmpdir ${imagePlan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`
72880
- );
72881
- }
72882
- }
74236
+ if (sigintHandler)
74237
+ process.off("SIGINT", sigintHandler);
74238
+ await cleanup();
72883
74239
  }
72884
74240
  }
72885
74241
  async function resolveImagePlan(lambda, options) {
@@ -72901,11 +74257,31 @@ async function resolveZipImagePlan(lambda, options) {
72901
74257
  }
72902
74258
  const image = resolveRuntimeImage(lambda.runtime);
72903
74259
  await pullImage(image, options.pull === false);
74260
+ const layerPlan = materializeLambdaLayers2(lambda.layers);
72904
74261
  return {
72905
74262
  image,
72906
74263
  mounts: [{ hostPath: codeDir, containerPath: "/var/task", readOnly: true }],
74264
+ extraMounts: layerPlan.mount ? [layerPlan.mount] : [],
72907
74265
  cmd: [lambda.handler],
72908
- ...inlineTmpDir !== void 0 && { inlineTmpDir }
74266
+ ...inlineTmpDir !== void 0 && { inlineTmpDir },
74267
+ ...layerPlan.tmpDir !== void 0 && { layersTmpDir: layerPlan.tmpDir }
74268
+ };
74269
+ }
74270
+ function materializeLambdaLayers2(layers) {
74271
+ if (layers.length === 0)
74272
+ return {};
74273
+ if (layers.length === 1) {
74274
+ return {
74275
+ mount: { hostPath: layers[0].assetPath, containerPath: "/opt", readOnly: true }
74276
+ };
74277
+ }
74278
+ const tmpDir = mkdtempSync3(path2.join(tmpdir3(), "cdkd-local-invoke-layers-"));
74279
+ for (const layer of layers) {
74280
+ cpSync2(layer.assetPath, tmpDir, { recursive: true, force: true });
74281
+ }
74282
+ return {
74283
+ mount: { hostPath: tmpDir, containerPath: "/opt", readOnly: true },
74284
+ tmpDir
72909
74285
  };
72910
74286
  }
72911
74287
  async function resolveContainerImagePlan(lambda, options) {
@@ -72938,6 +74314,7 @@ async function resolveContainerImagePlan(lambda, options) {
72938
74314
  return {
72939
74315
  image: imageRef,
72940
74316
  mounts: [],
74317
+ extraMounts: [],
72941
74318
  cmd: lambda.imageConfig.command ?? [],
72942
74319
  platform,
72943
74320
  ...lambda.imageConfig.entryPoint && lambda.imageConfig.entryPoint.length > 0 && {
@@ -73256,7 +74633,7 @@ function reorderArgs(argv) {
73256
74633
  }
73257
74634
  async function main() {
73258
74635
  const program = new Command16();
73259
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.72.0");
74636
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.74.0");
73260
74637
  program.addCommand(createBootstrapCommand());
73261
74638
  program.addCommand(createSynthCommand());
73262
74639
  program.addCommand(createListCommand());