@go-to-k/cdkd 0.125.0 → 0.126.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
@@ -65,6 +65,7 @@ import { promisify } from "node:util";
65
65
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
66
66
  import { readFile } from "fs/promises";
67
67
  import { createServer as createServer$1 } from "node:http";
68
+ import { createServer as createServer$2 } from "node:https";
68
69
  import * as chokidar from "chokidar";
69
70
 
70
71
  //#region src/cli/options.ts
@@ -40562,7 +40563,7 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
40562
40563
  stage: ctx.route.stage,
40563
40564
  time: formatRequestTime(now),
40564
40565
  timeEpoch: now.getTime(),
40565
- authentication: null,
40566
+ authentication: req.clientCert ? { clientCert: req.clientCert } : null,
40566
40567
  authorizer: null
40567
40568
  },
40568
40569
  body,
@@ -40612,7 +40613,8 @@ function buildRestV1Event(req, ctx, opts = {}) {
40612
40613
  httpMethod: req.method.toUpperCase(),
40613
40614
  identity: {
40614
40615
  sourceIp: req.sourceIp ?? "127.0.0.1",
40615
- userAgent: headers["user-agent"] ?? ""
40616
+ userAgent: headers["user-agent"] ?? "",
40617
+ ...req.clientCert && { clientCert: req.clientCert }
40616
40618
  },
40617
40619
  path: `/${ctx.route.stage}${ctx.matchedPath}`,
40618
40620
  protocol: "HTTP/1.1",
@@ -42804,12 +42806,20 @@ function amzDateOutsideSkew(amzDate, now) {
42804
42806
  async function startApiServer(opts) {
42805
42807
  const logger = getLogger().child("start-api");
42806
42808
  let currentState = opts.state;
42807
- const server = createServer$1((req, res) => {
42809
+ const requestHandler = (req, res) => {
42808
42810
  handleRequest(req, res, currentState, opts).catch((err) => {
42809
42811
  logger.error(`Unhandled request error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
42810
42812
  if (!res.headersSent) writeError(res, 502);
42811
42813
  });
42812
- });
42814
+ };
42815
+ const server = opts.mtls ? createServer$2({
42816
+ requestCert: true,
42817
+ rejectUnauthorized: true,
42818
+ ca: opts.mtls.caPem,
42819
+ cert: opts.mtls.certPem,
42820
+ key: opts.mtls.keyPem
42821
+ }, requestHandler) : createServer$1(requestHandler);
42822
+ const scheme = opts.mtls ? "https" : "http";
42813
42823
  server.on("connection", (socket) => {
42814
42824
  socket.setNoDelay(true);
42815
42825
  });
@@ -42831,6 +42841,7 @@ async function startApiServer(opts) {
42831
42841
  return {
42832
42842
  port: actualPort,
42833
42843
  host: actualHost,
42844
+ scheme,
42834
42845
  server,
42835
42846
  close: async () => {
42836
42847
  if (closed) return;
@@ -42886,12 +42897,14 @@ async function handleRequest(req, res, state, opts) {
42886
42897
  return;
42887
42898
  }
42888
42899
  const authorizer = state.routes.find((r) => r.route.declaredAt === match.route.declaredAt && r.route.method === match.route.method)?.authorizer;
42900
+ const clientCert = opts.mtls ? extractClientCert(req) : void 0;
42889
42901
  const snapshot = {
42890
42902
  method,
42891
42903
  rawUrl,
42892
42904
  headers: collectHeaders(req),
42893
42905
  body: bodyBuf,
42894
- ...req.socket.remoteAddress !== void 0 && { sourceIp: req.socket.remoteAddress }
42906
+ ...req.socket.remoteAddress !== void 0 && { sourceIp: req.socket.remoteAddress },
42907
+ ...clientCert && { clientCert }
42895
42908
  };
42896
42909
  const matchCtx = {
42897
42910
  route: match.route,
@@ -43357,6 +43370,135 @@ function writeMockCorsPreflight(res, preflight) {
43357
43370
  for (const [name, value] of Object.entries(preflight.headers)) res.setHeader(name, value);
43358
43371
  res.end();
43359
43372
  }
43373
+ /**
43374
+ * Extract the verified client certificate from a request's TLS socket.
43375
+ *
43376
+ * Pre-conditions (load-bearing — caller MUST gate on `opts.mtls`):
43377
+ * - The server was started with `https.createServer({requestCert: true,
43378
+ * rejectUnauthorized: true, ...})`, so the TLS handshake has
43379
+ * already rejected unknown-CA / self-signed / missing-cert clients
43380
+ * by the time `handleRequest` runs. Any peer cert we see here is
43381
+ * structurally valid against the supplied CA bundle — we do NOT
43382
+ * re-verify in code.
43383
+ *
43384
+ * Returns `undefined` when the request was not over a TLS socket (the
43385
+ * caller should NOT call this on plain-HTTP requests; the gate is the
43386
+ * `opts.mtls` check in `handleRequest`).
43387
+ *
43388
+ * The returned shape is the AWS-canonical
43389
+ * `requestContext.identity.clientCert` per
43390
+ * https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mutual-tls.html#api-gateway-mutual-tls-event-shape:
43391
+ *
43392
+ * {
43393
+ * clientCertPem: "-----BEGIN CERTIFICATE-----\n...",
43394
+ * subjectDN: "CN=client,O=example,C=US",
43395
+ * issuerDN: "CN=My CA,O=example,C=US",
43396
+ * serialNumber: "01:23:45:67:...",
43397
+ * validity: { notBefore: "May 22 03:30:00 2026 GMT",
43398
+ * notAfter: "May 22 03:30:00 2027 GMT" }
43399
+ * }
43400
+ *
43401
+ * Exported for unit testing — the helper is pure-functional given a
43402
+ * cert object and never touches the network.
43403
+ */
43404
+ function extractClientCert(req) {
43405
+ const socket = req.socket;
43406
+ if (typeof socket.getPeerCertificate !== "function") return void 0;
43407
+ return peerCertificateToAws(socket.getPeerCertificate(false));
43408
+ }
43409
+ /**
43410
+ * Convert Node's `PeerCertificate` object to the AWS-canonical
43411
+ * `clientCert` event shape. Exported separately from
43412
+ * {@link extractClientCert} so the conversion can be unit-tested
43413
+ * against a synthetic cert object without a real TLS socket.
43414
+ *
43415
+ * Returns `undefined` when the cert is empty (`getPeerCertificate`
43416
+ * returns `{}` when there is no peer cert). Otherwise emits every
43417
+ * field defined by the AWS shape, falling back to `''` for missing
43418
+ * subject / issuer DN segments so handlers do not need to null-check.
43419
+ */
43420
+ function peerCertificateToAws(cert) {
43421
+ if (!cert || typeof cert !== "object") return void 0;
43422
+ if (Object.keys(cert).length === 0) return void 0;
43423
+ const c = cert;
43424
+ const subject = c["subject"];
43425
+ const issuer = c["issuer"];
43426
+ const raw = c["raw"];
43427
+ const subjectDN = formatDN(subject);
43428
+ const issuerDN = formatDN(issuer);
43429
+ const serialNumber = typeof c["serialNumber"] === "string" ? c["serialNumber"] : "";
43430
+ const validity = {
43431
+ notBefore: typeof c["valid_from"] === "string" ? c["valid_from"] : "",
43432
+ notAfter: typeof c["valid_to"] === "string" ? c["valid_to"] : ""
43433
+ };
43434
+ return {
43435
+ clientCertPem: Buffer.isBuffer(raw) ? derBufferToPem(raw) : "",
43436
+ subjectDN,
43437
+ issuerDN,
43438
+ serialNumber,
43439
+ validity
43440
+ };
43441
+ }
43442
+ /**
43443
+ * Format a Node `subject` / `issuer` object (e.g.
43444
+ * `{C: 'US', O: 'example', CN: 'client'}`) as the canonical
43445
+ * comma-separated DN string AWS emits (`CN=client,O=example,C=US`).
43446
+ *
43447
+ * Ordering follows AWS / OpenSSL convention: CN first, then OU, O, L,
43448
+ * ST, C. Fields the cert does not declare are skipped silently.
43449
+ */
43450
+ function formatDN(dn) {
43451
+ if (!dn || typeof dn !== "object") return "";
43452
+ const obj = dn;
43453
+ const order = [
43454
+ "CN",
43455
+ "OU",
43456
+ "O",
43457
+ "L",
43458
+ "ST",
43459
+ "C"
43460
+ ];
43461
+ const parts = [];
43462
+ for (const key of order) {
43463
+ const v = obj[key];
43464
+ if (typeof v === "string" && v.length > 0) parts.push(`${key}=${v}`);
43465
+ }
43466
+ return parts.join(",");
43467
+ }
43468
+ /**
43469
+ * Encode a DER-encoded certificate Buffer as PEM. We wrap the base64
43470
+ * in 64-char-per-line segments the way `openssl x509` does so the
43471
+ * round-trip looks like what AWS API Gateway emits.
43472
+ */
43473
+ function derBufferToPem(der) {
43474
+ const b64 = der.toString("base64");
43475
+ const lines = [];
43476
+ for (let i = 0; i < b64.length; i += 64) lines.push(b64.slice(i, i + 64));
43477
+ return `-----BEGIN CERTIFICATE-----\n${lines.join("\n")}\n-----END CERTIFICATE-----\n`;
43478
+ }
43479
+ /**
43480
+ * Read mTLS materials from disk. Each path is a PEM file. The function
43481
+ * throws a wrapped error naming the offending path on `ENOENT` /
43482
+ * permission failures so the CLI surfaces a clear error before the
43483
+ * server starts.
43484
+ *
43485
+ * Exported for the CLI's resolve-then-construct flow + for unit tests.
43486
+ */
43487
+ function readMtlsMaterialsFromDisk(opts) {
43488
+ return {
43489
+ caPem: readPemOrThrow(opts.truststorePath, "--mtls-truststore"),
43490
+ certPem: readPemOrThrow(opts.certPath, "--mtls-cert"),
43491
+ keyPem: readPemOrThrow(opts.keyPath, "--mtls-key")
43492
+ };
43493
+ }
43494
+ function readPemOrThrow(path, flagName) {
43495
+ try {
43496
+ return readFileSync(path);
43497
+ } catch (err) {
43498
+ const msg = err instanceof Error ? err.message : String(err);
43499
+ throw new Error(`${flagName}: cannot read PEM file at '${path}': ${msg}`);
43500
+ }
43501
+ }
43360
43502
 
43361
43503
  //#endregion
43362
43504
  //#region src/local/api-server-grouping.ts
@@ -43955,6 +44097,8 @@ async function localStartApiCommand(target, options) {
43955
44097
  const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
43956
44098
  const basePort = parseInt(options.port, 10);
43957
44099
  if (!Number.isFinite(basePort) || basePort < 0 || basePort > 65535) throw new Error(`--port must be 0..65535 (got ${options.port}).`);
44100
+ const mtlsConfig = resolveMtlsConfig(options);
44101
+ if (mtlsConfig) logger.info("mTLS enabled: client certificates required (chain check against --mtls-truststore at TLS handshake).");
43958
44102
  const initialGroups = groupRoutesByServer(initialMaterial.routes);
43959
44103
  const servers = [];
43960
44104
  let nextPort = basePort;
@@ -43976,6 +44120,7 @@ async function localStartApiCommand(target, options) {
43976
44120
  state: groupState,
43977
44121
  rieTimeoutMs,
43978
44122
  host: options.host,
44123
+ ...mtlsConfig && { mtls: mtlsConfig },
43979
44124
  port: basePort === 0 ? 0 : nextPort,
43980
44125
  authorizerCache,
43981
44126
  jwksCache,
@@ -43993,7 +44138,7 @@ async function localStartApiCommand(target, options) {
43993
44138
  printPerServerRouteTables(servers);
43994
44139
  warnUnsupportedRoutes(servers.flatMap((s) => s.group.routes.map((r) => r.route)), logger);
43995
44140
  logger.info(`Per-Lambda concurrency: ${perLambdaConcurrency} (override with --per-lambda-concurrency)`);
43996
- for (const { group, server } of servers) process.stdout.write(`Server listening on http://${server.host}:${server.port} (${group.displayName})\n`);
44141
+ for (const { group, server } of servers) process.stdout.write(`Server listening on ${server.scheme}://${server.host}:${server.port} (${group.displayName})\n`);
43997
44142
  process.stdout.write("^C to stop and clean up containers.\n");
43998
44143
  let watcher;
43999
44144
  let reloadChain = Promise.resolve();
@@ -44714,10 +44859,36 @@ function parseDebugPort(raw) {
44714
44859
  return parsed;
44715
44860
  }
44716
44861
  /**
44862
+ * Resolve the mTLS configuration from CLI options. Returns `undefined`
44863
+ * when none of the three `--mtls-*` flags is set (the server stays
44864
+ * plain-HTTP). When any of the three is set, ALL THREE must be set —
44865
+ * partial configurations are rejected at parse time so the server
44866
+ * never boots in a half-configured state.
44867
+ *
44868
+ * Exported for unit testing.
44869
+ */
44870
+ function resolveMtlsConfig(options) {
44871
+ const present = [];
44872
+ const absent = [];
44873
+ if (options.mtlsTruststore !== void 0 && options.mtlsTruststore !== "") present.push("--mtls-truststore");
44874
+ else absent.push("--mtls-truststore");
44875
+ if (options.mtlsCert !== void 0 && options.mtlsCert !== "") present.push("--mtls-cert");
44876
+ else absent.push("--mtls-cert");
44877
+ if (options.mtlsKey !== void 0 && options.mtlsKey !== "") present.push("--mtls-key");
44878
+ else absent.push("--mtls-key");
44879
+ if (present.length === 0) return void 0;
44880
+ if (absent.length > 0) throw new Error(`mTLS configuration is incomplete: ${present.join(", ")} set but ${absent.join(", ")} missing. All three of --mtls-truststore, --mtls-cert, and --mtls-key must be set together to enable mTLS, or all three left unset for plain HTTP.`);
44881
+ return readMtlsMaterialsFromDisk({
44882
+ truststorePath: options.mtlsTruststore,
44883
+ certPath: options.mtlsCert,
44884
+ keyPath: options.mtlsKey
44885
+ });
44886
+ }
44887
+ /**
44717
44888
  * Builder for the `start-api` subcommand. Wired up by `local.ts`.
44718
44889
  */
44719
44890
  function createLocalStartApiCommand() {
44720
- const startApi = new Command("start-api").description("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, Cognito User Pool / HTTP v2 JWT authorizers, and REST v1 AWS_IAM (SigV4 signature verification only — IAM policy evaluation is NOT emulated; see docs/local-emulation.md). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — 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.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(localStartApiCommand));
44891
+ const startApi = new Command("start-api").description("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, Cognito User Pool / HTTP v2 JWT authorizers, and REST v1 AWS_IAM (SigV4 signature verification only — IAM policy evaluation is NOT emulated; see docs/local-emulation.md). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — 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.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).addOption(new Option("--mtls-truststore <path>", "PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj \"/CN=cdkd-local-ca\" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj \"/CN=localhost\"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj \"/CN=client\"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...")).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(localStartApiCommand));
44721
44892
  [
44722
44893
  ...commonOptions,
44723
44894
  ...appOptions,
@@ -47768,7 +47939,7 @@ function reorderArgs(argv) {
47768
47939
  */
47769
47940
  async function main() {
47770
47941
  const program = new Command();
47771
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.125.0");
47942
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.126.0");
47772
47943
  program.addCommand(createBootstrapCommand());
47773
47944
  program.addCommand(createSynthCommand());
47774
47945
  program.addCommand(createListCommand());