@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 +178 -29
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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 (
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
42020
|
-
*
|
|
42021
|
-
*
|
|
42022
|
-
*
|
|
42023
|
-
*
|
|
42024
|
-
*
|
|
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
|
|
42032
|
-
const
|
|
42033
|
-
|
|
42034
|
-
|
|
42035
|
-
|
|
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
|
-
|
|
42039
|
-
|
|
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 =
|
|
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.
|
|
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());
|