@go-to-k/cdkd 0.137.0 → 0.137.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -41144,11 +41144,19 @@ function collectAuthRoutesForApi(apiLogicalId, template, _stackName) {
41144
41144
  const props = resource.Properties ?? {};
41145
41145
  if (pickRefLogicalId$2(props["ApiId"]) !== apiLogicalId) continue;
41146
41146
  const authType = props["AuthorizationType"];
41147
- if (typeof authType !== "string" || authType.length === 0) continue;
41148
- if (authType === "NONE") continue;
41147
+ if (authType === void 0) continue;
41149
41148
  const routeKey = props["RouteKey"];
41149
+ const routeKeyForReport = typeof routeKey === "string" ? routeKey : "<unknown>";
41150
+ if (typeof authType !== "string" || authType.length === 0) {
41151
+ result.push({
41152
+ routeKey: routeKeyForReport,
41153
+ authorizationType: "<intrinsic-or-malformed>"
41154
+ });
41155
+ continue;
41156
+ }
41157
+ if (authType === "NONE") continue;
41150
41158
  result.push({
41151
- routeKey: typeof routeKey === "string" ? routeKey : "<unknown>",
41159
+ routeKey: routeKeyForReport,
41152
41160
  authorizationType: authType
41153
41161
  });
41154
41162
  }
@@ -41506,6 +41514,24 @@ function parseConnectionsPath(url) {
41506
41514
  return { connectionId: decoded };
41507
41515
  }
41508
41516
  /**
41517
+ * Build the per-Lambda env-var URL the cdkd local server injects as
41518
+ * `AWS_ENDPOINT_URL_APIGATEWAYMANAGEMENTAPI`. The URL MUST include the
41519
+ * `/<stage>` segment to mirror the AWS-deployed apigatewaymanagementapi
41520
+ * endpoint `https://<api-id>.execute-api.<region>.amazonaws.com/<stage>`:
41521
+ * SDK clients built from `domainName + stage` produce
41522
+ * `POST /<stage>/@connections/<id>`, and the local `parseConnectionsPath`
41523
+ * regex above requires the matching prefix. Without `/<stage>` the
41524
+ * deployed-shape SDK call hits a 404 against the local parser
41525
+ * (BLOCKER B1, #526).
41526
+ *
41527
+ * Lives next to `parseConnectionsPath` so the producer + consumer of
41528
+ * the URL shape stay in lockstep — change one, the other's tests fail.
41529
+ * Issue #537 item 7.
41530
+ */
41531
+ function buildMgmtEndpointEnvUrl(host, port, stage) {
41532
+ return `http://${host}:${port}/${stage}`;
41533
+ }
41534
+ /**
41509
41535
  * `decodeURIComponent` throws `URIError` on malformed input
41510
41536
  * (`%`-escape with non-hex tail). We treat that as a not-found rather
41511
41537
  * than a server error — symmetric with AWS-deployed behavior, which
@@ -41632,6 +41658,38 @@ function writeJson(res, status, body) {
41632
41658
  res.end(json);
41633
41659
  }
41634
41660
 
41661
+ //#endregion
41662
+ //#region src/local/websocket-body.ts
41663
+ /**
41664
+ * Convert a ws-emitted message buffer into the AWS-canonical event
41665
+ * body + `isBase64Encoded` discriminator. Text frames (opcode 0x1) pass
41666
+ * through as UTF-8 with `isBase64Encoded: false`; binary frames
41667
+ * (opcode 0x2) are base64-encoded with `isBase64Encoded: true`. Matches
41668
+ * AWS-deployed WebSocket API event shape exactly — handlers decode via
41669
+ * `Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')`.
41670
+ *
41671
+ * Closes the data-integrity bug where every byte > 0x7F on a binary
41672
+ * frame was silently corrupted by handlers that trusted the previously
41673
+ * hardcoded `isBase64Encoded: false` flag and UTF-8-decoded the
41674
+ * base64-encoded body.
41675
+ *
41676
+ * Lives in its own module so the B4 regression test can install a
41677
+ * `vi.fn()` spy that intercepts EVERY call — same-module references in
41678
+ * `websocket-server.ts` would bypass the export-binding spy. See
41679
+ * Issue #537 item 6.
41680
+ */
41681
+ function bufferToBody(raw, isBinary) {
41682
+ const buf = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
41683
+ if (isBinary) return {
41684
+ body: buf.toString("base64"),
41685
+ isBase64Encoded: true
41686
+ };
41687
+ return {
41688
+ body: buf.toString("utf-8"),
41689
+ isBase64Encoded: false
41690
+ };
41691
+ }
41692
+
41635
41693
  //#endregion
41636
41694
  //#region src/local/websocket-server.ts
41637
41695
  /**
@@ -41682,6 +41740,16 @@ function attachWebSocketServer(opts) {
41682
41740
  apisByPath.set(cfg.apiPath, cfg);
41683
41741
  apiPaths.push(cfg.apiPath);
41684
41742
  }
41743
+ const dispatchChainsByConnection = /* @__PURE__ */ new Map();
41744
+ const enqueueDispatch = (connectionId, work) => {
41745
+ const next = (dispatchChainsByConnection.get(connectionId) ?? Promise.resolve()).then(work);
41746
+ dispatchChainsByConnection.set(connectionId, next);
41747
+ next.finally(() => {
41748
+ if (dispatchChainsByConnection.get(connectionId) === next) dispatchChainsByConnection.delete(connectionId);
41749
+ });
41750
+ return next;
41751
+ };
41752
+ const inFlightDisconnects = /* @__PURE__ */ new Set();
41685
41753
  const upgradeListener = (req, socket, head) => {
41686
41754
  const pathOnly = (req.url ?? "/").split("?", 1)[0];
41687
41755
  const cfg = apisByPath.get(pathOnly);
@@ -41719,7 +41787,7 @@ function attachWebSocketServer(opts) {
41719
41787
  if (preVerdictFrames.length >= MAX_PRE_VERDICT_FRAMES) {
41720
41788
  if (!preVerdictOverflow) {
41721
41789
  preVerdictOverflow = true;
41722
- logger.warn(`WebSocket connection ${connectionId}: pre-verdict message buffer overflowed (>${MAX_PRE_VERDICT_FRAMES} frames). Excess frames dropped — client is sending faster than the $connect handler can resolve.`);
41790
+ logger.warn(`WebSocket connection ${connectionId}: pre-verdict message buffer overflowed (>${MAX_PRE_VERDICT_FRAMES} frames). Excess frames dropped — client is sending faster than the $connect handler can resolve. (api=${cfg.api.declaredAt})`);
41723
41791
  }
41724
41792
  return;
41725
41793
  }
@@ -41761,7 +41829,7 @@ function attachWebSocketServer(opts) {
41761
41829
  ws.on("message", (raw, isBinary) => {
41762
41830
  const { body, isBase64Encoded } = bufferToBody(raw, isBinary);
41763
41831
  logger.debug(`WebSocket message received for connection ${connectionId}: ${body.slice(0, 200)}`);
41764
- dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41832
+ enqueueDispatch(connectionId, () => dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41765
41833
  logger.error(`WebSocket message dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
41766
41834
  try {
41767
41835
  ws.send(JSON.stringify({
@@ -41770,11 +41838,16 @@ function attachWebSocketServer(opts) {
41770
41838
  requestId: randomUUID()
41771
41839
  }));
41772
41840
  } catch {}
41773
- });
41841
+ }));
41774
41842
  });
41775
41843
  ws.on("close", (code, reason) => {
41776
- onDisconnect(connectionId, cfg, handshakeSnapshot, code, reason.toString("utf-8")).catch((err) => {
41844
+ const reasonText = reason.toString("utf-8");
41845
+ const disconnectPromise = enqueueDispatch(connectionId, () => onDisconnect(connectionId, cfg, handshakeSnapshot, code, reasonText).catch((err) => {
41777
41846
  logger.warn(`WebSocket $disconnect dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.message : String(err)}`);
41847
+ }));
41848
+ inFlightDisconnects.add(disconnectPromise);
41849
+ disconnectPromise.finally(() => {
41850
+ inFlightDisconnects.delete(disconnectPromise);
41778
41851
  });
41779
41852
  });
41780
41853
  ws.on("error", (err) => {
@@ -41782,9 +41855,9 @@ function attachWebSocketServer(opts) {
41782
41855
  });
41783
41856
  for (const frame of preVerdictFrames) {
41784
41857
  const { body, isBase64Encoded } = bufferToBody(frame.raw, frame.isBinary);
41785
- dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41858
+ enqueueDispatch(connectionId, () => dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41786
41859
  logger.error(`WebSocket buffered-message dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
41787
- });
41860
+ }));
41788
41861
  }
41789
41862
  preVerdictFrames.length = 0;
41790
41863
  };
@@ -41830,6 +41903,7 @@ function attachWebSocketServer(opts) {
41830
41903
  });
41831
41904
  await invokeRoute(disconnectRoute.targetLambdaLogicalId, event, opts.pool, rieTimeoutMs);
41832
41905
  };
41906
+ const SHUTDOWN_DRAIN_MS = 5e3;
41833
41907
  let closed = false;
41834
41908
  return {
41835
41909
  registry,
@@ -41852,6 +41926,11 @@ function attachWebSocketServer(opts) {
41852
41926
  }, 5e3).unref();
41853
41927
  }));
41854
41928
  await Promise.all(closes);
41929
+ if (inFlightDisconnects.size > 0) {
41930
+ const drainStartCount = inFlightDisconnects.size;
41931
+ const drainComplete = Promise.allSettled(Array.from(inFlightDisconnects));
41932
+ if (await Promise.race([drainComplete.then(() => "complete"), new Promise((resolve) => setTimeout(() => resolve("timeout"), SHUTDOWN_DRAIN_MS).unref())]) === "timeout" && inFlightDisconnects.size > 0) logger.warn(`WebSocket graceful shutdown drained for ${SHUTDOWN_DRAIN_MS}ms; ${inFlightDisconnects.size}/${drainStartCount} $disconnect handlers still in flight — leaking past shutdown.`);
41933
+ }
41855
41934
  await new Promise((resolve) => {
41856
41935
  wss.close(() => resolve());
41857
41936
  });
@@ -42015,28 +42094,93 @@ function safeDecode$1(s) {
42015
42094
  return null;
42016
42095
  }
42017
42096
  }
42097
+
42098
+ //#endregion
42099
+ //#region src/local/docker-version.ts
42100
+ /**
42101
+ * Lower bound for `--add-host=<name>:host-gateway` support. The
42102
+ * `host-gateway` magic alias was introduced in Docker 20.10 (October
42103
+ * 2020) and is the load-bearing primitive cdkd uses to let Lambda
42104
+ * containers reach the host's `cdkd local start-api` server on Linux
42105
+ * native dockerd. Without it, the AWS_ENDPOINT_URL_APIGATEWAYMANAGEMENTAPI
42106
+ * override fails with `ENOTFOUND host.docker.internal` at SDK-call time.
42107
+ *
42108
+ * Docker Desktop (macOS / Windows) ships `host.docker.internal` as
42109
+ * a built-in alias regardless of the engine version, but the probe
42110
+ * still fires there to keep the error path uniform — the `host-gateway`
42111
+ * flag itself is harmless on Docker Desktop.
42112
+ *
42113
+ * Issue #527 M2.
42114
+ */
42115
+ const HOST_GATEWAY_MIN_VERSION = {
42116
+ major: 20,
42117
+ minor: 10,
42118
+ patch: 0
42119
+ };
42018
42120
  /**
42019
- * Convert a ws-emitted message buffer into the AWS-canonical event
42020
- * body + `isBase64Encoded` discriminator. Text frames (opcode 0x1) pass
42021
- * through as UTF-8 with `isBase64Encoded: false`; binary frames
42022
- * (opcode 0x2) are base64-encoded with `isBase64Encoded: true`. Matches
42023
- * AWS-deployed WebSocket API event shape exactly handlers decode via
42024
- * `Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')`.
42025
- *
42026
- * Closes the data-integrity bug where every byte > 0x7F on a binary
42027
- * frame was silently corrupted by handlers that trusted the previously
42028
- * hardcoded `isBase64Encoded: false` flag and UTF-8-decoded the
42029
- * base64-encoded body.
42121
+ * Parse a Docker server version string (`20.10.21` / `24.0.7-rd` /
42122
+ * `27.3.1+podman` etc.) into a comparable `{major, minor, patch}` tuple.
42123
+ * Returns `null` on any unparseable input the caller treats that as
42124
+ * "version unknown, skip the comparison and let the user proceed with
42125
+ * a warn" rather than hard-failing on a Docker-compatible CLI binary
42126
+ * that doesn't follow Docker's version-string conventions
42127
+ * (e.g. podman / finch).
42030
42128
  */
42031
- function bufferToBody(raw, isBinary) {
42032
- const buf = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
42033
- if (isBinary) return {
42034
- body: buf.toString("base64"),
42035
- isBase64Encoded: true
42129
+ function parseDockerVersion(raw) {
42130
+ const trimmed = raw.trim();
42131
+ const match = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(trimmed);
42132
+ if (!match) return null;
42133
+ return {
42134
+ major: Number(match[1]),
42135
+ minor: Number(match[2]),
42136
+ patch: match[3] !== void 0 ? Number(match[3]) : 0
42137
+ };
42138
+ }
42139
+ /**
42140
+ * Compare two `ParsedDockerVersion` tuples. Returns negative when `a <
42141
+ * b`, zero when equal, positive when `a > b`. Patch-level differences
42142
+ * are part of the ordering so a future bump (e.g. 20.10.0 -> 20.10.5
42143
+ * to fix a CVE-related regression) can be expressed if needed.
42144
+ */
42145
+ function compareDockerVersions(a, b) {
42146
+ if (a.major !== b.major) return a.major - b.major;
42147
+ if (a.minor !== b.minor) return a.minor - b.minor;
42148
+ return a.patch - b.patch;
42149
+ }
42150
+ /**
42151
+ * Probe the Docker server's version to gate the `--add-host=...:host-gateway`
42152
+ * mapping that WebSocket Lambda containers need to reach the host
42153
+ * server. Issued ONCE per `cdkd local start-api` invocation at WebSocket
42154
+ * attach time — HTTP-only / REST-only sessions skip the probe entirely.
42155
+ *
42156
+ * Throws when:
42157
+ * 1. The docker subprocess itself fails (binary missing, daemon down,
42158
+ * permission error) — the caller's catch surfaces the original
42159
+ * error so the user knows to install / start Docker.
42160
+ * 2. The probe succeeds but the parsed version is < the supported
42161
+ * minimum — caller decides whether to error or warn (the WebSocket
42162
+ * attach loop errors; HTTP-only sessions never call this).
42163
+ *
42164
+ * Implementation: `docker version --format '{{.Server.Version}}'`
42165
+ * returns the daemon's version (not the client's) so a brand-new
42166
+ * client against an old daemon is still caught.
42167
+ */
42168
+ async function probeHostGatewaySupport() {
42169
+ const rawVersion = (await runDockerStreaming([
42170
+ "version",
42171
+ "--format",
42172
+ "{{.Server.Version}}"
42173
+ ], { streamLive: false })).stdout.trim();
42174
+ const parsed = parseDockerVersion(rawVersion);
42175
+ if (rawVersion === "") return {
42176
+ rawVersion,
42177
+ parsed: null,
42178
+ supported: false
42036
42179
  };
42037
42180
  return {
42038
- body: buf.toString("utf-8"),
42039
- isBase64Encoded: false
42181
+ rawVersion,
42182
+ parsed,
42183
+ supported: parsed === null || compareDockerVersions(parsed, HOST_GATEWAY_MIN_VERSION) >= 0
42040
42184
  };
42041
42185
  }
42042
42186
 
@@ -48007,6 +48151,11 @@ async function localStartApiCommand(target, options) {
48007
48151
  const wsServers = [];
48008
48152
  const initialWsApis = initialMaterial.webSocketApis ?? [];
48009
48153
  warnUnsupportedWebSocketApis(initialWsApis, logger);
48154
+ if (initialWsApis.filter((api) => !api.unsupported).length > 0) {
48155
+ const probe = await probeHostGatewaySupport();
48156
+ if (!probe.supported) throw new Error(`cdkd local start-api requires Docker ${HOST_GATEWAY_MIN_VERSION.major}.${HOST_GATEWAY_MIN_VERSION.minor}+ for WebSocket API support (--add-host=host.docker.internal:host-gateway needs the 20.10 host-gateway alias). Detected server version: ${probe.rawVersion || "<empty — daemon unreachable or output stripped>"}. Upgrade Docker, or remove the WebSocket API from this app to fall back to HTTP-only start-api.`);
48157
+ if (probe.parsed === null) logger.warn(`Docker server version "${probe.rawVersion}" did not match the canonical "<major>.<minor>" shape; assuming host-gateway support. If WebSocket containers fail to reach the local server, verify your Docker-compatible CLI honors --add-host=host.docker.internal:host-gateway.`);
48158
+ }
48010
48159
  for (const api of initialWsApis) {
48011
48160
  if (api.unsupported) continue;
48012
48161
  const wsLambdaIds = new Set(api.routes.map((r) => r.targetLambdaLogicalId));
@@ -48052,7 +48201,7 @@ async function localStartApiCommand(target, options) {
48052
48201
  }]
48053
48202
  });
48054
48203
  registryRef = attached;
48055
- const mgmtEndpoint = `http://host.docker.internal:${started.port}/${api.stage}`;
48204
+ const mgmtEndpoint = buildMgmtEndpointEnvUrl("host.docker.internal", started.port, api.stage);
48056
48205
  const hostGatewayMapping = [{
48057
48206
  host: "host.docker.internal",
48058
48207
  ip: "host-gateway"
@@ -53981,7 +54130,7 @@ function reorderArgs(argv) {
53981
54130
  */
53982
54131
  async function main() {
53983
54132
  const program = new Command();
53984
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.137.0");
54133
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.137.2");
53985
54134
  program.addCommand(createBootstrapCommand());
53986
54135
  program.addCommand(createSynthCommand());
53987
54136
  program.addCommand(createListCommand());