@go-to-k/cdkd 0.137.1 → 0.137.3

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
@@ -41121,7 +41121,7 @@ function discoverOneApi(logicalId, resource, template, stackName) {
41121
41121
  apiLogicalId: logicalId,
41122
41122
  apiStackName: stackName,
41123
41123
  declaredAt,
41124
- ...apiCdkPath !== "" && { apiCdkPath },
41124
+ ...apiCdkPath !== void 0 && { apiCdkPath },
41125
41125
  routeSelectionExpression,
41126
41126
  stage,
41127
41127
  routes,
@@ -41306,12 +41306,45 @@ function pickRefLogicalId$2(value) {
41306
41306
  }
41307
41307
  function readApiCdkPath(logicalId, template) {
41308
41308
  const resource = template.Resources?.[logicalId];
41309
- if (!resource) return "";
41310
- return readCdkPath(resource);
41309
+ if (!resource) return void 0;
41310
+ const path = readCdkPath(resource);
41311
+ return path === "" ? void 0 : path;
41311
41312
  }
41312
41313
 
41313
41314
  //#endregion
41314
41315
  //#region src/local/websocket-event.ts
41316
+ /**
41317
+ * AWS API Gateway WebSocket event-payload builders.
41318
+ *
41319
+ * Spec: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-mapping-template-reference.html
41320
+ *
41321
+ * Three event types — CONNECT / MESSAGE / DISCONNECT — each carry a
41322
+ * shared {@link WebSocketRequestContext} plus per-event fields.
41323
+ *
41324
+ * Fields cdkd populates locally vs mocks (matches design Q1 in
41325
+ * `docs/design/462-websocket-api.md`):
41326
+ *
41327
+ * | Field | Source |
41328
+ * |--------------------------------------|-----------------------------------------|
41329
+ * | `connectionId` | UUID v4 generated at `$connect` |
41330
+ * | `requestId` / `extendedRequestId` | Generated UUID per event |
41331
+ * | `messageId` (MESSAGE only) | Generated UUID per event |
41332
+ * | `requestTime` / `requestTimeEpoch` | `Date.now()` at build time |
41333
+ * | `connectedAt` | Captured at `$connect` |
41334
+ * | `stage` | Resolved Stage Name; `'local'` default |
41335
+ * | `apiId` | `'local'` (mock) |
41336
+ * | `domainName` | `'localhost'` (mock) |
41337
+ * | `identity.sourceIp` | `req.socket.remoteAddress` (real) |
41338
+ * | `identity.userAgent` | Upgrade `User-Agent` header (real) |
41339
+ * | `headers`/`queryStringParameters` | Parsed from upgrade `req` (real) |
41340
+ * | `authorizer` | `null` in v1 (deferred) |
41341
+ *
41342
+ * Per-event `eventType` / `routeKey` / `messageDirection` are fixed by
41343
+ * the lifecycle stage, NOT by the route's `routeKey` field — `routeKey`
41344
+ * carries which user-declared route fired ("$connect" / "$disconnect" /
41345
+ * "$default" / custom).
41346
+ */
41347
+ const MOCK_ACCOUNT_ID$1 = "123456789012";
41315
41348
  const MOCK_DOMAIN_NAME$1 = "localhost";
41316
41349
  const MOCK_API_ID$1 = "local";
41317
41350
  /**
@@ -41338,6 +41371,7 @@ function buildRequestContext(routeKey, eventType, connectionId, connectedAt, sta
41338
41371
  apiId: MOCK_API_ID$1,
41339
41372
  authorizer: null,
41340
41373
  identity: {
41374
+ accountId: MOCK_ACCOUNT_ID$1,
41341
41375
  sourceIp: snapshot.sourceIp ?? "127.0.0.1",
41342
41376
  userAgent: snapshot.userAgent ?? ""
41343
41377
  }
@@ -41740,6 +41774,16 @@ function attachWebSocketServer(opts) {
41740
41774
  apisByPath.set(cfg.apiPath, cfg);
41741
41775
  apiPaths.push(cfg.apiPath);
41742
41776
  }
41777
+ const dispatchChainsByConnection = /* @__PURE__ */ new Map();
41778
+ const enqueueDispatch = (connectionId, work) => {
41779
+ const next = (dispatchChainsByConnection.get(connectionId) ?? Promise.resolve()).then(work);
41780
+ dispatchChainsByConnection.set(connectionId, next);
41781
+ next.finally(() => {
41782
+ if (dispatchChainsByConnection.get(connectionId) === next) dispatchChainsByConnection.delete(connectionId);
41783
+ });
41784
+ return next;
41785
+ };
41786
+ const inFlightDisconnects = /* @__PURE__ */ new Set();
41743
41787
  const upgradeListener = (req, socket, head) => {
41744
41788
  const pathOnly = (req.url ?? "/").split("?", 1)[0];
41745
41789
  const cfg = apisByPath.get(pathOnly);
@@ -41819,7 +41863,7 @@ function attachWebSocketServer(opts) {
41819
41863
  ws.on("message", (raw, isBinary) => {
41820
41864
  const { body, isBase64Encoded } = bufferToBody(raw, isBinary);
41821
41865
  logger.debug(`WebSocket message received for connection ${connectionId}: ${body.slice(0, 200)}`);
41822
- dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41866
+ enqueueDispatch(connectionId, () => dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41823
41867
  logger.error(`WebSocket message dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
41824
41868
  try {
41825
41869
  ws.send(JSON.stringify({
@@ -41828,11 +41872,16 @@ function attachWebSocketServer(opts) {
41828
41872
  requestId: randomUUID()
41829
41873
  }));
41830
41874
  } catch {}
41831
- });
41875
+ }));
41832
41876
  });
41833
41877
  ws.on("close", (code, reason) => {
41834
- onDisconnect(connectionId, cfg, handshakeSnapshot, code, reason.toString("utf-8")).catch((err) => {
41878
+ const reasonText = reason.toString("utf-8");
41879
+ const disconnectPromise = enqueueDispatch(connectionId, () => onDisconnect(connectionId, cfg, handshakeSnapshot, code, reasonText).catch((err) => {
41835
41880
  logger.warn(`WebSocket $disconnect dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.message : String(err)}`);
41881
+ }));
41882
+ inFlightDisconnects.add(disconnectPromise);
41883
+ disconnectPromise.finally(() => {
41884
+ inFlightDisconnects.delete(disconnectPromise);
41836
41885
  });
41837
41886
  });
41838
41887
  ws.on("error", (err) => {
@@ -41840,9 +41889,9 @@ function attachWebSocketServer(opts) {
41840
41889
  });
41841
41890
  for (const frame of preVerdictFrames) {
41842
41891
  const { body, isBase64Encoded } = bufferToBody(frame.raw, frame.isBinary);
41843
- dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41892
+ enqueueDispatch(connectionId, () => dispatchMessage(connectionId, cfg, body, isBase64Encoded, handshakeSnapshot).catch((err) => {
41844
41893
  logger.error(`WebSocket buffered-message dispatch failed (connection ${connectionId}): ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
41845
- });
41894
+ }));
41846
41895
  }
41847
41896
  preVerdictFrames.length = 0;
41848
41897
  };
@@ -41888,6 +41937,7 @@ function attachWebSocketServer(opts) {
41888
41937
  });
41889
41938
  await invokeRoute(disconnectRoute.targetLambdaLogicalId, event, opts.pool, rieTimeoutMs);
41890
41939
  };
41940
+ const SHUTDOWN_DRAIN_MS = 5e3;
41891
41941
  let closed = false;
41892
41942
  return {
41893
41943
  registry,
@@ -41910,6 +41960,11 @@ function attachWebSocketServer(opts) {
41910
41960
  }, 5e3).unref();
41911
41961
  }));
41912
41962
  await Promise.all(closes);
41963
+ if (inFlightDisconnects.size > 0) {
41964
+ const drainStartCount = inFlightDisconnects.size;
41965
+ const drainComplete = Promise.allSettled(Array.from(inFlightDisconnects));
41966
+ 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.`);
41967
+ }
41913
41968
  await new Promise((resolve) => {
41914
41969
  wss.close(() => resolve());
41915
41970
  });
@@ -42074,6 +42129,95 @@ function safeDecode$1(s) {
42074
42129
  }
42075
42130
  }
42076
42131
 
42132
+ //#endregion
42133
+ //#region src/local/docker-version.ts
42134
+ /**
42135
+ * Lower bound for `--add-host=<name>:host-gateway` support. The
42136
+ * `host-gateway` magic alias was introduced in Docker 20.10 (October
42137
+ * 2020) and is the load-bearing primitive cdkd uses to let Lambda
42138
+ * containers reach the host's `cdkd local start-api` server on Linux
42139
+ * native dockerd. Without it, the AWS_ENDPOINT_URL_APIGATEWAYMANAGEMENTAPI
42140
+ * override fails with `ENOTFOUND host.docker.internal` at SDK-call time.
42141
+ *
42142
+ * Docker Desktop (macOS / Windows) ships `host.docker.internal` as
42143
+ * a built-in alias regardless of the engine version, but the probe
42144
+ * still fires there to keep the error path uniform — the `host-gateway`
42145
+ * flag itself is harmless on Docker Desktop.
42146
+ *
42147
+ * Issue #527 M2.
42148
+ */
42149
+ const HOST_GATEWAY_MIN_VERSION = {
42150
+ major: 20,
42151
+ minor: 10,
42152
+ patch: 0
42153
+ };
42154
+ /**
42155
+ * Parse a Docker server version string (`20.10.21` / `24.0.7-rd` /
42156
+ * `27.3.1+podman` etc.) into a comparable `{major, minor, patch}` tuple.
42157
+ * Returns `null` on any unparseable input — the caller treats that as
42158
+ * "version unknown, skip the comparison and let the user proceed with
42159
+ * a warn" rather than hard-failing on a Docker-compatible CLI binary
42160
+ * that doesn't follow Docker's version-string conventions
42161
+ * (e.g. podman / finch).
42162
+ */
42163
+ function parseDockerVersion(raw) {
42164
+ const trimmed = raw.trim();
42165
+ const match = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(trimmed);
42166
+ if (!match) return null;
42167
+ return {
42168
+ major: Number(match[1]),
42169
+ minor: Number(match[2]),
42170
+ patch: match[3] !== void 0 ? Number(match[3]) : 0
42171
+ };
42172
+ }
42173
+ /**
42174
+ * Compare two `ParsedDockerVersion` tuples. Returns negative when `a <
42175
+ * b`, zero when equal, positive when `a > b`. Patch-level differences
42176
+ * are part of the ordering so a future bump (e.g. 20.10.0 -> 20.10.5
42177
+ * to fix a CVE-related regression) can be expressed if needed.
42178
+ */
42179
+ function compareDockerVersions(a, b) {
42180
+ if (a.major !== b.major) return a.major - b.major;
42181
+ if (a.minor !== b.minor) return a.minor - b.minor;
42182
+ return a.patch - b.patch;
42183
+ }
42184
+ /**
42185
+ * Probe the Docker server's version to gate the `--add-host=...:host-gateway`
42186
+ * mapping that WebSocket Lambda containers need to reach the host
42187
+ * server. Issued ONCE per `cdkd local start-api` invocation at WebSocket
42188
+ * attach time — HTTP-only / REST-only sessions skip the probe entirely.
42189
+ *
42190
+ * Throws when:
42191
+ * 1. The docker subprocess itself fails (binary missing, daemon down,
42192
+ * permission error) — the caller's catch surfaces the original
42193
+ * error so the user knows to install / start Docker.
42194
+ * 2. The probe succeeds but the parsed version is < the supported
42195
+ * minimum — caller decides whether to error or warn (the WebSocket
42196
+ * attach loop errors; HTTP-only sessions never call this).
42197
+ *
42198
+ * Implementation: `docker version --format '{{.Server.Version}}'`
42199
+ * returns the daemon's version (not the client's) so a brand-new
42200
+ * client against an old daemon is still caught.
42201
+ */
42202
+ async function probeHostGatewaySupport() {
42203
+ const rawVersion = (await runDockerStreaming([
42204
+ "version",
42205
+ "--format",
42206
+ "{{.Server.Version}}"
42207
+ ], { streamLive: false })).stdout.trim();
42208
+ const parsed = parseDockerVersion(rawVersion);
42209
+ if (rawVersion === "") return {
42210
+ rawVersion,
42211
+ parsed: null,
42212
+ supported: false
42213
+ };
42214
+ return {
42215
+ rawVersion,
42216
+ parsed,
42217
+ supported: parsed === null || compareDockerVersions(parsed, HOST_GATEWAY_MIN_VERSION) >= 0
42218
+ };
42219
+ }
42220
+
42077
42221
  //#endregion
42078
42222
  //#region src/local/vtl-engine.ts
42079
42223
  /** Error thrown when a template references an unsupported VTL feature. */
@@ -48041,6 +48185,11 @@ async function localStartApiCommand(target, options) {
48041
48185
  const wsServers = [];
48042
48186
  const initialWsApis = initialMaterial.webSocketApis ?? [];
48043
48187
  warnUnsupportedWebSocketApis(initialWsApis, logger);
48188
+ if (initialWsApis.filter((api) => !api.unsupported).length > 0) {
48189
+ const probe = await probeHostGatewaySupport();
48190
+ 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.`);
48191
+ 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.`);
48192
+ }
48044
48193
  for (const api of initialWsApis) {
48045
48194
  if (api.unsupported) continue;
48046
48195
  const wsLambdaIds = new Set(api.routes.map((r) => r.targetLambdaLogicalId));
@@ -54015,7 +54164,7 @@ function reorderArgs(argv) {
54015
54164
  */
54016
54165
  async function main() {
54017
54166
  const program = new Command();
54018
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.137.1");
54167
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.137.3");
54019
54168
  program.addCommand(createBootstrapCommand());
54020
54169
  program.addCommand(createSynthCommand());
54021
54170
  program.addCommand(createListCommand());