cdk-local 0.57.0 → 0.59.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/README.md +2 -2
- package/dist/cli.js +3 -3
- package/dist/{cloud-map-resolver-BDAxtbvB.js → cloud-map-resolver-DjKCYyIq.js} +132 -18
- package/dist/cloud-map-resolver-DjKCYyIq.js.map +1 -0
- package/dist/{error-handler-CwzKivb8.d.ts → error-handler-BVgybgd9.d.ts} +22 -2
- package/dist/error-handler-BVgybgd9.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -2
- package/dist/{local-list-DmEjNav4.js → local-list-ChGfKSkV.js} +325 -48
- package/dist/local-list-ChGfKSkV.js.map +1 -0
- package/package.json +1 -1
- package/dist/cloud-map-resolver-BDAxtbvB.js.map +0 -1
- package/dist/error-handler-CwzKivb8.d.ts.map +0 -1
- package/dist/local-list-DmEjNav4.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as runDockerStreaming, c as getEmbedConfig, r as getDockerCmd, s as getLogger, u as setEmbedConfig } from "./docker-cmd-voNPrcRh.js";
|
|
2
|
-
import { $n as
|
|
2
|
+
import { $n as applyRoleArnIfSet, $t as derivePartitionAndUrlSuffix, At as waitForRieReady, Bn as resolveAgentCoreTarget, Bt as execEnvForSecrets, Cn as resolveMultiTarget, Dt as AssetManifestLoader, En as listTargets, Et as singleFlight, Ft as buildDockerImage, Gn as matchStacks, Gt as streamLogs, Hn as derivePseudoParametersFromRegion, Ht as pullImage, In as AGENTCORE_MCP_PROTOCOL, It as DockerRunnerError, Jn as resolveCdkPathToLogicalIds, Jt as resolveRuntimeImage, Kn as buildCdkPathIndex, Kt as resolveRuntimeCodeMountPath, Lt as SENSITIVE_ENV_KEYS, Mt as buildContainerImage, Nn as AGENTCORE_A2A_PROTOCOL, Nt as parseEcrUri, Ot as getDockerImageBySourceHash, Pn as AGENTCORE_AGUI_PROTOCOL, Pt as pullEcrImage, Q as verifyJwtViaDiscovery, Qn as withErrorHandling, Qt as checkVolumeHostPath, Rt as appendEnvFlags, Sn as Synthesizer, Tn as countTargets, Tt as writeProfileCredentialsFile, Ut as removeContainer, Vn as resolveLambdaTarget, Vt as pickFreePort, Wt as runDetached, Xn as LocalInvokeBuildError, Xt as TASK_ROLE_ACCOUNT_PLACEHOLDER, Y as createJwksCache, Yn as CdkLocalError, Yt as EcsTaskResolutionError, Z as verifyJwtAuthorizer, Zn as LocalStartServiceError, Zt as applyCrossStackResolverToTask, _t as invokeAgentCore, an as substituteAgainstStateAsync, ar as warnIfDeprecatedRegion, at as invokeAgentCoreWs, bn as resolveApp, c as resolveProfileCredentials$1, cn as resolveEnvVars, ct as a2aInvokeOnce, dn as createLocalStateProvider, en as detectEcsImageResolutionNeeds, er as appOptions, ft as mcpInvokeOnce, ht as signAgentCoreInvocation, i as getPublishedHostPort, ir as parseContextOptions, jt as architectureToPlatform, kt as invokeRie, ln as materializeLayerFromArn, lt as MCP_CONTAINER_PORT, mn as resolveCfnFallbackRegion, n as CloudMapRegistry, nn as resolveEcsTaskTarget, nr as contextOptions, ot as A2A_CONTAINER_PORT, pn as rejectExplicitCfnStackWithMultipleStacks, qn as readCdkPathOrUndefined, qt as resolveRuntimeFileExtension, r as getContainerNetworkIp, rn as applyDeployedEnvFallback, rr as deprecatedRegionOption, sn as substituteEnvVarsFromStateAsync, st as A2A_PATH, t as buildCloudMapIndex, tn as parseEcsTarget, tr as commonOptions, ut as MCP_PATH, vt as waitForAgentCorePing, wn as resolveSingleTarget, xt as buildAgentCoreCodeImage, yt as downloadAndExtractS3Bundle, zn as pickAgentCoreCandidateStack, zt as ensureDockerAvailable } from "./cloud-map-resolver-DjKCYyIq.js";
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
@@ -681,15 +681,21 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
681
681
|
const resolved = resolveAgentCoreTarget(resolvedTarget, stacks, imageContext);
|
|
682
682
|
logger.info(`Target: ${resolved.stack.stackName}/${resolved.logicalId} (${resolved.protocol})`);
|
|
683
683
|
const isMcp = resolved.protocol === "MCP";
|
|
684
|
-
|
|
684
|
+
const isA2a = resolved.protocol === "A2A";
|
|
685
|
+
if (resolved.protocol === "AGUI") logger.info("AGUI runtime: routing through the HTTP /invocations + /ws path (AG-UI wire is SSE / WebSocket on port 8080).");
|
|
686
|
+
if ((isMcp || isA2a) && options.ws) logger.warn(`--ws applies only to the HTTP / AGUI protocols; ignoring it for this ${resolved.protocol} runtime.`);
|
|
685
687
|
if (options.wsInteractive && !options.ws) logger.warn("--ws-interactive is meaningful only with --ws; ignoring.");
|
|
686
|
-
if (options.sigv4 && (isMcp || options.ws)) logger.warn("--sigv4 signs the HTTP /invocations request only; ignoring it for the " + (isMcp ? "MCP" : "/ws WebSocket") + " path.");
|
|
688
|
+
if (options.sigv4 && (isMcp || isA2a || options.ws)) logger.warn("--sigv4 signs the HTTP /invocations request only; ignoring it for the " + (isMcp ? "MCP" : isA2a ? "A2A" : "/ws WebSocket") + " path.");
|
|
687
689
|
const sessionId = options.sessionId ?? randomUUID();
|
|
688
690
|
const event = await readEvent(options);
|
|
689
691
|
const mcpRequest = isMcp ? buildMcpRequest(event) : void 0;
|
|
692
|
+
const a2aRequest = isA2a ? buildA2aRequest(event) : void 0;
|
|
690
693
|
let authorization;
|
|
691
|
-
if (isMcp) {
|
|
692
|
-
if (resolved.jwtAuthorizer || options.bearerToken)
|
|
694
|
+
if (isMcp || isA2a) {
|
|
695
|
+
if (resolved.jwtAuthorizer || options.bearerToken) {
|
|
696
|
+
const pathLabel = isMcp ? MCP_PATH : "/";
|
|
697
|
+
logger.info(`${resolved.protocol} runtime: invoking the local container's ${pathLabel} directly (vanilla ${resolved.protocol}). An inbound JWT / --bearer-token is an AgentCore managed-plane concern and is not applied locally.`);
|
|
698
|
+
}
|
|
693
699
|
} else authorization = await resolveInboundAuthorization(resolved, options);
|
|
694
700
|
await resolveFromS3BucketIntrinsic(resolved, stateProvider, loadedState, imageContext);
|
|
695
701
|
const image = await resolveAgentCoreImage(resolved, options, loadedState);
|
|
@@ -697,7 +703,8 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
697
703
|
const hostPort = await pickFreePort();
|
|
698
704
|
const containerHost = options.containerHost;
|
|
699
705
|
const containerName = `${getEmbedConfig().resourceNamePrefix}-agentcore-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
700
|
-
const
|
|
706
|
+
const containerPort = isMcp ? MCP_CONTAINER_PORT : isA2a ? A2A_CONTAINER_PORT : void 0;
|
|
707
|
+
const containerPortLabel = isMcp ? `${MCP_CONTAINER_PORT}${MCP_PATH}` : isA2a ? `${A2A_CONTAINER_PORT}${"/"}` : "8080";
|
|
701
708
|
logger.info(`Starting agent container (image=${image}, port=${hostPort} -> ${containerPortLabel})...`);
|
|
702
709
|
containerId = await runDetached({
|
|
703
710
|
image,
|
|
@@ -708,7 +715,7 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
708
715
|
host: containerHost,
|
|
709
716
|
platform: options.platform,
|
|
710
717
|
name: containerName,
|
|
711
|
-
...
|
|
718
|
+
...containerPort !== void 0 && { containerPort },
|
|
712
719
|
...sensitiveEnvKeys.size > 0 && { sensitiveEnvKeys }
|
|
713
720
|
});
|
|
714
721
|
stopLogs = streamLogs(containerId);
|
|
@@ -721,6 +728,11 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
721
728
|
const mcp = await mcpInvokeOnce(containerHost, hostPort, mcpRequest, { requestTimeoutMs: options.timeout });
|
|
722
729
|
await new Promise((r) => setTimeout(r, 250));
|
|
723
730
|
emitMcpResult(mcp);
|
|
731
|
+
} else if (isA2a && a2aRequest) {
|
|
732
|
+
logger.info(`A2A request: ${a2aRequest.method}`);
|
|
733
|
+
const a2a = await a2aInvokeOnce(containerHost, hostPort, a2aRequest, { requestTimeoutMs: options.timeout });
|
|
734
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
735
|
+
emitA2aResult(a2a);
|
|
724
736
|
} else if (options.ws) {
|
|
725
737
|
await waitForAgentCorePing(containerHost, hostPort);
|
|
726
738
|
const frameSource = options.wsInteractive ? readStdinLines() : void 0;
|
|
@@ -1239,6 +1251,39 @@ function emitMcpResult(result) {
|
|
|
1239
1251
|
}
|
|
1240
1252
|
process.stdout.write(`${result.raw}\n`);
|
|
1241
1253
|
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Build the JSON-RPC request to send to an A2A runtime from `--event`:
|
|
1256
|
+
* - no `--event` (empty object) → `agent/getCard` (discover the agent's card),
|
|
1257
|
+
* - an object with a string `method` → that method + its `params`,
|
|
1258
|
+
* - anything else → a fail-fast error.
|
|
1259
|
+
*
|
|
1260
|
+
* Exported for unit testing.
|
|
1261
|
+
*/
|
|
1262
|
+
function buildA2aRequest(event) {
|
|
1263
|
+
if (event === void 0 || event === null) return {
|
|
1264
|
+
method: "agent/getCard",
|
|
1265
|
+
params: {}
|
|
1266
|
+
};
|
|
1267
|
+
if (typeof event !== "object" || Array.isArray(event)) throw new CdkLocalError("A2A --event must be a JSON object describing a JSON-RPC request (e.g. {\"method\":\"tasks/send\",\"params\":{\"id\":\"...\",\"message\":{...}}}).", "LOCAL_INVOKE_AGENTCORE_A2A_EVENT_INVALID");
|
|
1268
|
+
const obj = event;
|
|
1269
|
+
if (Object.keys(obj).length === 0) return {
|
|
1270
|
+
method: "agent/getCard",
|
|
1271
|
+
params: {}
|
|
1272
|
+
};
|
|
1273
|
+
if (typeof obj["method"] !== "string") throw new CdkLocalError(`A2A --event must include a string "method" (a JSON-RPC method such as "agent/getCard" or "tasks/send"). Got keys: ${Object.keys(obj).join(", ")}.`, "LOCAL_INVOKE_AGENTCORE_A2A_EVENT_INVALID");
|
|
1274
|
+
return {
|
|
1275
|
+
method: obj["method"],
|
|
1276
|
+
...obj["params"] !== void 0 && { params: obj["params"] }
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
/** Print the A2A JSON-RPC response; exit 1 when it carried a JSON-RPC error. */
|
|
1280
|
+
function emitA2aResult(result) {
|
|
1281
|
+
if (!result.ok) {
|
|
1282
|
+
getLogger().warn("A2A server returned a JSON-RPC error.");
|
|
1283
|
+
process.exitCode = 1;
|
|
1284
|
+
}
|
|
1285
|
+
process.stdout.write(`${result.raw}\n`);
|
|
1286
|
+
}
|
|
1242
1287
|
/** Map a `--platform` value to the architecture `buildContainerImage` expects. */
|
|
1243
1288
|
function platformToArchitecture(platform) {
|
|
1244
1289
|
return platform === "linux/amd64" ? "x86_64" : "arm64";
|
|
@@ -3798,25 +3843,67 @@ function handleProxyRequest(req, res, opts) {
|
|
|
3798
3843
|
...req.socket.remoteAddress !== void 0 && { sourceIp: req.socket.remoteAddress }
|
|
3799
3844
|
});
|
|
3800
3845
|
if (!action) return reply404(req, res, opts);
|
|
3801
|
-
if (action.
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3846
|
+
if (action.auth) {
|
|
3847
|
+
const auth = action.auth;
|
|
3848
|
+
return auth.check(req.headers).then((result) => {
|
|
3849
|
+
if (!result.allow) {
|
|
3850
|
+
req.resume();
|
|
3851
|
+
writeUnauthorized(res, auth.realm, result.reason);
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
return serveAction(req, res, action, opts);
|
|
3855
|
+
}).catch((err) => {
|
|
3856
|
+
getLogger().child("front-door").debug(`auth gate error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3857
|
+
req.resume();
|
|
3858
|
+
writeUnauthorized(res, auth.realm, "auth check failed");
|
|
3859
|
+
});
|
|
3811
3860
|
}
|
|
3812
|
-
|
|
3813
|
-
return handlePoolRequest(req, res, picked.pool, opts);
|
|
3861
|
+
return serveAction(req, res, action, opts);
|
|
3814
3862
|
}
|
|
3815
3863
|
const target = resolveDispatchTarget(opts, url);
|
|
3816
3864
|
if (!target) return reply404(req, res, opts);
|
|
3817
3865
|
if (target.kind === "lambda") return handleLambdaRequest(req, res, target.lambda, opts);
|
|
3818
3866
|
return handlePoolRequest(req, res, target.pool, opts);
|
|
3819
3867
|
}
|
|
3868
|
+
/**
|
|
3869
|
+
* Dispatch a resolved {@link RouteAction} — the path the route resolver
|
|
3870
|
+
* returned (after any auth gate). Splits forward (ECS pool or Lambda invoker)
|
|
3871
|
+
* from redirect / fixed-response, mirroring what the inline logic used to do.
|
|
3872
|
+
*/
|
|
3873
|
+
function serveAction(req, res, action, opts) {
|
|
3874
|
+
if (action.kind === "redirect" || action.kind === "fixed-response") {
|
|
3875
|
+
req.resume();
|
|
3876
|
+
if (action.kind === "redirect") writeRedirect(res, action, req, opts.listenerPort, opts.tls ? "https" : "http");
|
|
3877
|
+
else writeFixedResponse(res, action);
|
|
3878
|
+
return Promise.resolve();
|
|
3879
|
+
}
|
|
3880
|
+
const picked = pickWeightedTarget(action.pools);
|
|
3881
|
+
if (!picked) {
|
|
3882
|
+
writeError(res, 502, `No forward target selected behind ${opts.label} (every weighted target has weight 0).`);
|
|
3883
|
+
return Promise.resolve();
|
|
3884
|
+
}
|
|
3885
|
+
if ("lambda" in picked) return handleLambdaRequest(req, res, picked.lambda, opts);
|
|
3886
|
+
return handlePoolRequest(req, res, picked.pool, opts);
|
|
3887
|
+
}
|
|
3888
|
+
/**
|
|
3889
|
+
* Write a 401 with `WWW-Authenticate: Bearer realm="..."`. ALB itself would
|
|
3890
|
+
* redirect to the IdP's authorize endpoint; for local dev parity we deny
|
|
3891
|
+
* loudly so the user notices and either supplies `--bearer-token` / passes a
|
|
3892
|
+
* fresh Authorization header / disables the guard with `--no-verify-auth`.
|
|
3893
|
+
*/
|
|
3894
|
+
function writeUnauthorized(res, realm, reason) {
|
|
3895
|
+
const body = reason && reason !== "" ? reason : "Unauthorized";
|
|
3896
|
+
res.writeHead(401, {
|
|
3897
|
+
"content-type": "text/plain; charset=utf-8",
|
|
3898
|
+
"content-length": String(Buffer.byteLength(`${body}\n`)),
|
|
3899
|
+
"www-authenticate": `Bearer realm="${escapeRealmQuotes(realm)}"`
|
|
3900
|
+
});
|
|
3901
|
+
res.end(`${body}\n`);
|
|
3902
|
+
}
|
|
3903
|
+
/** Escape `"` in the realm so the `WWW-Authenticate` header parses cleanly. */
|
|
3904
|
+
function escapeRealmQuotes(realm) {
|
|
3905
|
+
return realm.replace(/"/g, "\\\"");
|
|
3906
|
+
}
|
|
3820
3907
|
/** Reply 404 — an ALB listener with no matching rule and no default action. */
|
|
3821
3908
|
function reply404(req, res, opts) {
|
|
3822
3909
|
writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
@@ -4685,6 +4772,98 @@ function createFrontDoorLambdaRunner(lambda, opts) {
|
|
|
4685
4772
|
};
|
|
4686
4773
|
}
|
|
4687
4774
|
|
|
4775
|
+
//#endregion
|
|
4776
|
+
//#region src/local/front-door-auth.ts
|
|
4777
|
+
/**
|
|
4778
|
+
* Build the front-door's auth-check callback for an
|
|
4779
|
+
* `authenticate-cognito` / `authenticate-oidc` guard.
|
|
4780
|
+
*
|
|
4781
|
+
* Local-dev parity model: the cloud-side ALB authenticates the user via a
|
|
4782
|
+
* full OAuth roundtrip (redirect to the IdP's authorize endpoint, callback,
|
|
4783
|
+
* AWSELBAuthSessionCookie issuance). The local front-door does NOT reproduce
|
|
4784
|
+
* the roundtrip — it accepts EITHER a Bearer JWT (verified against the
|
|
4785
|
+
* Cognito JWKS / OIDC discovery URL just like API Gateway's JWT authorizer)
|
|
4786
|
+
* OR an `AWSELBAuthSessionCookie-*` cookie pass-through (the user is acting
|
|
4787
|
+
* as if already signed in via the deployed ALB — `--bearer-token` makes the
|
|
4788
|
+
* Bearer-JWT path the headline path; the cookie pass-through is convenience
|
|
4789
|
+
* for hitting the local front-door from a browser session that already
|
|
4790
|
+
* authenticated through the deployed ALB).
|
|
4791
|
+
*
|
|
4792
|
+
* `--no-verify-auth` (the `noVerifyAuth` flag) short-circuits everything to
|
|
4793
|
+
* `allow: true` — explicitly off-switching the guard for local dev where
|
|
4794
|
+
* you do not want to mint a Bearer token at all.
|
|
4795
|
+
*
|
|
4796
|
+
* `--bearer-token <jwt>` (the `bearerToken` arg) makes the supplied token
|
|
4797
|
+
* the default `Authorization` value when the inbound request has none.
|
|
4798
|
+
*/
|
|
4799
|
+
function buildAuthCheck(guard, jwksCache, opts = {}) {
|
|
4800
|
+
const realm = guard.label;
|
|
4801
|
+
if (opts.noVerifyAuth === true) return {
|
|
4802
|
+
realm,
|
|
4803
|
+
check: async () => ({ allow: true })
|
|
4804
|
+
};
|
|
4805
|
+
const warned = opts.warned ?? /* @__PURE__ */ new Set();
|
|
4806
|
+
const sessionCookiePrefix = guard.sessionCookieName;
|
|
4807
|
+
const injectedBearer = opts.bearerToken;
|
|
4808
|
+
return {
|
|
4809
|
+
realm,
|
|
4810
|
+
check: async (headers) => {
|
|
4811
|
+
const cookieHeader = headerValue(headers["cookie"]);
|
|
4812
|
+
if (cookieHeader && cookieHasSessionPrefix(cookieHeader, sessionCookiePrefix)) return { allow: true };
|
|
4813
|
+
let authorization = headerValue(headers["authorization"]);
|
|
4814
|
+
if ((!authorization || authorization === "") && injectedBearer !== void 0) authorization = `Bearer ${injectedBearer}`;
|
|
4815
|
+
if (!authorization || authorization === "") return {
|
|
4816
|
+
allow: false,
|
|
4817
|
+
reason: "No Bearer token presented. Supply Authorization: Bearer <jwt> or pass --bearer-token <jwt>."
|
|
4818
|
+
};
|
|
4819
|
+
if (!authorization.toLowerCase().startsWith("bearer ")) return {
|
|
4820
|
+
allow: false,
|
|
4821
|
+
reason: "Authorization scheme is not Bearer; the ALB authenticate-* guard only accepts Bearer JWTs."
|
|
4822
|
+
};
|
|
4823
|
+
try {
|
|
4824
|
+
if ((await verifyJwtAuthorizer({
|
|
4825
|
+
kind: "jwt",
|
|
4826
|
+
logicalId: guard.label,
|
|
4827
|
+
declaredAt: "start-alb authenticate-* action",
|
|
4828
|
+
issuer: guard.issuer,
|
|
4829
|
+
audience: [guard.audience],
|
|
4830
|
+
...guard.region !== void 0 && { region: guard.region },
|
|
4831
|
+
...guard.userPoolId !== void 0 && { userPoolId: guard.userPoolId }
|
|
4832
|
+
}, authorization, jwksCache, { warned })).allow) return { allow: true };
|
|
4833
|
+
return {
|
|
4834
|
+
allow: false,
|
|
4835
|
+
reason: "Bearer token rejected (signature / iss / aud / exp check failed)."
|
|
4836
|
+
};
|
|
4837
|
+
} catch (err) {
|
|
4838
|
+
getLogger().child("front-door-auth").debug(`auth check threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
4839
|
+
return {
|
|
4840
|
+
allow: false,
|
|
4841
|
+
reason: "Auth check failed."
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4847
|
+
function headerValue(raw) {
|
|
4848
|
+
if (raw === void 0) return void 0;
|
|
4849
|
+
if (Array.isArray(raw)) return raw[0];
|
|
4850
|
+
return raw;
|
|
4851
|
+
}
|
|
4852
|
+
/**
|
|
4853
|
+
* True iff the `Cookie` header contains a cookie whose name starts with
|
|
4854
|
+
* `<sessionCookiePrefix>` (ALB suffixes the cookie with `-0` / `-1` / ...
|
|
4855
|
+
* when the auth session payload exceeds the per-cookie size limit, so we
|
|
4856
|
+
* match the prefix, not an exact name).
|
|
4857
|
+
*/
|
|
4858
|
+
function cookieHasSessionPrefix(cookieHeader, sessionCookiePrefix) {
|
|
4859
|
+
for (const pair of cookieHeader.split(";")) {
|
|
4860
|
+
const eq = pair.indexOf("=");
|
|
4861
|
+
const name = (eq === -1 ? pair : pair.slice(0, eq)).trim();
|
|
4862
|
+
if (name === sessionCookiePrefix || name.startsWith(`${sessionCookiePrefix}-`)) return true;
|
|
4863
|
+
}
|
|
4864
|
+
return false;
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4688
4867
|
//#endregion
|
|
4689
4868
|
//#region src/cli/commands/ecs-service-emulator.ts
|
|
4690
4869
|
/**
|
|
@@ -4993,9 +5172,20 @@ async function buildFrontDoor(plan, options, logger) {
|
|
|
4993
5172
|
certPath: options.tlsCert,
|
|
4994
5173
|
keyPath: options.tlsKey
|
|
4995
5174
|
}) : void 0;
|
|
5175
|
+
const jwksCache = createJwksCache();
|
|
5176
|
+
const sharedWarned = /* @__PURE__ */ new Set();
|
|
5177
|
+
const authForGuard = (guard) => buildAuthCheck(guard, jwksCache, {
|
|
5178
|
+
...options.verifyAuth === false && { noVerifyAuth: true },
|
|
5179
|
+
...options.bearerToken !== void 0 && { bearerToken: options.bearerToken },
|
|
5180
|
+
warned: sharedWarned
|
|
5181
|
+
});
|
|
5182
|
+
const attachAuth = (action, guard) => guard ? {
|
|
5183
|
+
...action,
|
|
5184
|
+
auth: authForGuard(guard)
|
|
5185
|
+
} : action;
|
|
4996
5186
|
try {
|
|
4997
5187
|
for (const listener of plan.listeners) {
|
|
4998
|
-
const defaultRoute = listener.defaultAction ? toRouteAction(listener.defaultAction) : void 0;
|
|
5188
|
+
const defaultRoute = listener.defaultAction ? attachAuth(toRouteAction(listener.defaultAction), listener.defaultAuthGuard) : void 0;
|
|
4999
5189
|
const ruleRoutes = listener.rules.map((r) => ({
|
|
5000
5190
|
priority: r.priority,
|
|
5001
5191
|
pathPatterns: r.pathPatterns,
|
|
@@ -5004,7 +5194,7 @@ async function buildFrontDoor(plan, options, logger) {
|
|
|
5004
5194
|
httpRequestMethods: r.httpRequestMethods,
|
|
5005
5195
|
queryStringConditions: r.queryStringConditions,
|
|
5006
5196
|
sourceIpCidrs: r.sourceIpCidrs,
|
|
5007
|
-
target: toRouteAction(r.action)
|
|
5197
|
+
target: attachAuth(toRouteAction(r.action), r.authGuard)
|
|
5008
5198
|
}));
|
|
5009
5199
|
const route = (req) => matchAlbPathRule(req, ruleRoutes) ?? defaultRoute;
|
|
5010
5200
|
const tls = listener.protocol === "HTTPS" ? tlsMaterials : void 0;
|
|
@@ -5322,9 +5512,19 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
5322
5512
|
* mix ECS and Lambda targets. HTTPS listeners are served — local TLS
|
|
5323
5513
|
* termination uses a user-supplied or auto-generated self-signed cert/key
|
|
5324
5514
|
* pair (the deployed `Listener.Certificates[]` ACM ARNs are not fetched,
|
|
5325
|
-
* because ACM private keys are not retrievable by design).
|
|
5326
|
-
*
|
|
5327
|
-
* `authenticate-oidc` actions
|
|
5515
|
+
* because ACM private keys are not retrievable by design).
|
|
5516
|
+
*
|
|
5517
|
+
* `authenticate-cognito` / `authenticate-oidc` actions ARE served — they are
|
|
5518
|
+
* lifted from the `Actions[]` array into an `authGuard` attached to the
|
|
5519
|
+
* terminal action they wrap. The front-door enforces a Bearer-JWT check
|
|
5520
|
+
* locally (signature + `iss` + `exp` + `aud`) using the existing JWKS-cached
|
|
5521
|
+
* verifier; missing / invalid token -> 401 with `WWW-Authenticate: Bearer`.
|
|
5522
|
+
* Out of scope: the full OAuth roundtrip (authorize-endpoint redirect +
|
|
5523
|
+
* callback). The local-dev parity is "I have a JWT, let me through" — the
|
|
5524
|
+
* `--bearer-token <jwt>` flag is the convenience escape hatch; the
|
|
5525
|
+
* `--no-verify-auth` flag disables the guard entirely.
|
|
5526
|
+
*
|
|
5527
|
+
* Skipped with a warning: TLS listeners (NLB-style, not ALB).
|
|
5328
5528
|
*/
|
|
5329
5529
|
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
5330
5530
|
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
@@ -5356,7 +5556,7 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
5356
5556
|
continue;
|
|
5357
5557
|
}
|
|
5358
5558
|
if (protocol === "HTTPS" && Array.isArray(props["Certificates"]) && props["Certificates"].length > 0) warnings.push(`Listener '${listenerLogicalId}' on port ${port} declares ACM Certificates which are not retrievable locally. The front-door terminates TLS with --tls-cert/--tls-key or an auto-generated self-signed cert.`);
|
|
5359
|
-
const
|
|
5559
|
+
const defaultResolved = resolveAction(props["DefaultActions"], resources, tgToService, stackName, `Listener '${listenerLogicalId}' (port ${port}) default action`, warnings);
|
|
5360
5560
|
const rules = [];
|
|
5361
5561
|
for (const { ruleLogicalId, ruleProps } of rulesByListener.get(listenerLogicalId) ?? []) {
|
|
5362
5562
|
const priority = parsePriority(ruleProps["Priority"]);
|
|
@@ -5365,8 +5565,8 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
5365
5565
|
if (!parsed) continue;
|
|
5366
5566
|
const { pathPatterns, hostPatterns, httpHeaderConditions, httpRequestMethods, queryStringConditions, sourceIpCidrs } = parsed;
|
|
5367
5567
|
if (!(pathPatterns.length > 0 || hostPatterns.length > 0 || httpHeaderConditions.length > 0 || httpRequestMethods.length > 0 || queryStringConditions.length > 0 || sourceIpCidrs.length > 0)) continue;
|
|
5368
|
-
const
|
|
5369
|
-
if (!
|
|
5568
|
+
const ruleResolved = resolveAction(ruleProps["Actions"], resources, tgToService, stackName, `${ruleLabel} action`, warnings);
|
|
5569
|
+
if (!ruleResolved) continue;
|
|
5370
5570
|
rules.push({
|
|
5371
5571
|
priority,
|
|
5372
5572
|
pathPatterns,
|
|
@@ -5375,15 +5575,17 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
5375
5575
|
httpRequestMethods,
|
|
5376
5576
|
queryStringConditions,
|
|
5377
5577
|
sourceIpCidrs,
|
|
5378
|
-
action
|
|
5578
|
+
action: ruleResolved.action,
|
|
5579
|
+
...ruleResolved.authGuard ? { authGuard: ruleResolved.authGuard } : {}
|
|
5379
5580
|
});
|
|
5380
5581
|
}
|
|
5381
|
-
if (!
|
|
5582
|
+
if (!defaultResolved && rules.length === 0) continue;
|
|
5382
5583
|
listeners.push({
|
|
5383
5584
|
listenerPort: port,
|
|
5384
5585
|
listenerProtocol: protocol,
|
|
5385
5586
|
listenerLogicalId,
|
|
5386
|
-
...
|
|
5587
|
+
...defaultResolved ? { defaultAction: defaultResolved.action } : {},
|
|
5588
|
+
...defaultResolved?.authGuard ? { defaultAuthGuard: defaultResolved.authGuard } : {},
|
|
5387
5589
|
rules
|
|
5388
5590
|
});
|
|
5389
5591
|
}
|
|
@@ -5399,15 +5601,16 @@ function isApplicationLoadBalancer(resource) {
|
|
|
5399
5601
|
return type === void 0 || type === "application";
|
|
5400
5602
|
}
|
|
5401
5603
|
/**
|
|
5402
|
-
* Resolve a listener / rule `Actions` (or `DefaultActions`) array
|
|
5403
|
-
*
|
|
5404
|
-
*
|
|
5405
|
-
*
|
|
5406
|
-
*
|
|
5407
|
-
*
|
|
5604
|
+
* Resolve a listener / rule `Actions` (or `DefaultActions`) array. ALB allows
|
|
5605
|
+
* any number of `authenticate-cognito` / `authenticate-oidc` entries followed
|
|
5606
|
+
* by exactly one terminal action (`forward` / `redirect` / `fixed-response`).
|
|
5607
|
+
* The first parseable authenticate-* (last wins if multiple) becomes the
|
|
5608
|
+
* returned `authGuard`; the terminal action becomes `action`. Returns
|
|
5609
|
+
* `undefined` when no terminal action is resolvable.
|
|
5408
5610
|
*/
|
|
5409
5611
|
function resolveAction(actions, resources, tgToService, stackName, label, warnings) {
|
|
5410
5612
|
if (!Array.isArray(actions)) return void 0;
|
|
5613
|
+
let authGuard;
|
|
5411
5614
|
let sawAuthenticate = false;
|
|
5412
5615
|
for (const action of actions) {
|
|
5413
5616
|
if (!action || typeof action !== "object") continue;
|
|
@@ -5415,18 +5618,21 @@ function resolveAction(actions, resources, tgToService, stackName, label, warnin
|
|
|
5415
5618
|
const type = a["Type"];
|
|
5416
5619
|
if (type === "authenticate-cognito" || type === "authenticate-oidc") {
|
|
5417
5620
|
sawAuthenticate = true;
|
|
5621
|
+
const parsed = parseAuthenticateAction(a, type, label, warnings);
|
|
5622
|
+
if (parsed) authGuard = parsed;
|
|
5418
5623
|
continue;
|
|
5419
5624
|
}
|
|
5420
|
-
|
|
5421
|
-
if (type === "
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5625
|
+
let terminal;
|
|
5626
|
+
if (type === "forward") terminal = resolveForwardAction(a, resources, tgToService, stackName, label, warnings);
|
|
5627
|
+
else if (type === "redirect") terminal = resolveRedirectAction(a, label, warnings);
|
|
5628
|
+
else if (type === "fixed-response") terminal = resolveFixedResponseAction(a);
|
|
5629
|
+
else if (typeof type === "string") warnings.push(`${label} uses an unsupported action type '${type}'. The local ALB front-door supports forward / redirect / fixed-response actions only. Skipping it.`);
|
|
5630
|
+
if (terminal) return authGuard ? {
|
|
5631
|
+
action: terminal,
|
|
5632
|
+
authGuard
|
|
5633
|
+
} : { action: terminal };
|
|
5428
5634
|
}
|
|
5429
|
-
if (sawAuthenticate) warnings.push(`${label} is an authenticate-* action with no local-servable terminal action
|
|
5635
|
+
if (sawAuthenticate) warnings.push(`${label} is an authenticate-* action with no local-servable terminal action; skipping it.`);
|
|
5430
5636
|
}
|
|
5431
5637
|
/**
|
|
5432
5638
|
* Resolve a `forward` action into one or more weighted targets. Each target
|
|
@@ -5832,6 +6038,75 @@ function parsePriority(raw) {
|
|
|
5832
6038
|
if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
|
|
5833
6039
|
return Number.MAX_SAFE_INTEGER;
|
|
5834
6040
|
}
|
|
6041
|
+
/** ALB's default session cookie name prefix (suffixed `-0` / `-1` / ... by ALB). */
|
|
6042
|
+
const DEFAULT_ALB_SESSION_COOKIE = "AWSELBAuthSessionCookie";
|
|
6043
|
+
/**
|
|
6044
|
+
* Cognito User Pool ARN shape:
|
|
6045
|
+
* `arn:aws:cognito-idp:<region>:<account>:userpool/<pool-id>`
|
|
6046
|
+
* The pool id itself contains a `_`, but never a `/`, so anchoring on the
|
|
6047
|
+
* `userpool/` segment is robust.
|
|
6048
|
+
*/
|
|
6049
|
+
const COGNITO_USERPOOL_ARN = /^arn:[^:]+:cognito-idp:([^:]+):[^:]+:userpool\/([^/]+)$/;
|
|
6050
|
+
/**
|
|
6051
|
+
* Parse an `authenticate-cognito` / `authenticate-oidc` ALB action into a
|
|
6052
|
+
* resolvable {@link FrontDoorAuthGuard}, or `undefined` when the config is
|
|
6053
|
+
* unresolvable (a Ref / intrinsic in a required field, a malformed
|
|
6054
|
+
* UserPoolArn, etc.) — a warning is emitted in that case so the user knows
|
|
6055
|
+
* the guard was dropped (the terminal action will still serve unguarded).
|
|
6056
|
+
*/
|
|
6057
|
+
function parseAuthenticateAction(action, type, label, warnings) {
|
|
6058
|
+
if (type === "authenticate-cognito") {
|
|
6059
|
+
const cfg = action["AuthenticateCognitoConfig"];
|
|
6060
|
+
if (!cfg || typeof cfg !== "object") {
|
|
6061
|
+
warnings.push(`${label}: authenticate-cognito missing AuthenticateCognitoConfig; skipping guard.`);
|
|
6062
|
+
return;
|
|
6063
|
+
}
|
|
6064
|
+
const c = cfg;
|
|
6065
|
+
const userPoolArn = c["UserPoolArn"];
|
|
6066
|
+
const userPoolClientId = c["UserPoolClientId"];
|
|
6067
|
+
if (typeof userPoolArn !== "string" || typeof userPoolClientId !== "string") {
|
|
6068
|
+
warnings.push(`${label}: authenticate-cognito UserPoolArn / UserPoolClientId must be literal strings (Ref / intrinsics cannot be resolved by the local front-door); skipping guard.`);
|
|
6069
|
+
return;
|
|
6070
|
+
}
|
|
6071
|
+
const match = COGNITO_USERPOOL_ARN.exec(userPoolArn);
|
|
6072
|
+
if (!match) {
|
|
6073
|
+
warnings.push(`${label}: authenticate-cognito UserPoolArn '${userPoolArn}' is not in the expected arn:...:cognito-idp:<region>:<account>:userpool/<pool-id> shape; skipping guard.`);
|
|
6074
|
+
return;
|
|
6075
|
+
}
|
|
6076
|
+
const region = match[1];
|
|
6077
|
+
const userPoolId = match[2];
|
|
6078
|
+
const sessionCookieName = typeof c["SessionCookieName"] === "string" && c["SessionCookieName"] !== "" ? c["SessionCookieName"] : DEFAULT_ALB_SESSION_COOKIE;
|
|
6079
|
+
return {
|
|
6080
|
+
kind: "authenticate-cognito",
|
|
6081
|
+
issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`,
|
|
6082
|
+
audience: userPoolClientId,
|
|
6083
|
+
region,
|
|
6084
|
+
userPoolId,
|
|
6085
|
+
sessionCookieName,
|
|
6086
|
+
label: `authenticate-cognito (UserPool=${userPoolId})`
|
|
6087
|
+
};
|
|
6088
|
+
}
|
|
6089
|
+
const cfg = action["AuthenticateOidcConfig"];
|
|
6090
|
+
if (!cfg || typeof cfg !== "object") {
|
|
6091
|
+
warnings.push(`${label}: authenticate-oidc missing AuthenticateOidcConfig; skipping guard.`);
|
|
6092
|
+
return;
|
|
6093
|
+
}
|
|
6094
|
+
const c = cfg;
|
|
6095
|
+
const issuer = c["Issuer"];
|
|
6096
|
+
const clientId = c["ClientId"];
|
|
6097
|
+
if (typeof issuer !== "string" || typeof clientId !== "string") {
|
|
6098
|
+
warnings.push(`${label}: authenticate-oidc Issuer / ClientId must be literal strings (Ref / intrinsics cannot be resolved by the local front-door); skipping guard.`);
|
|
6099
|
+
return;
|
|
6100
|
+
}
|
|
6101
|
+
const sessionCookieName = typeof c["SessionCookieName"] === "string" && c["SessionCookieName"] !== "" ? c["SessionCookieName"] : DEFAULT_ALB_SESSION_COOKIE;
|
|
6102
|
+
return {
|
|
6103
|
+
kind: "authenticate-oidc",
|
|
6104
|
+
issuer: issuer.replace(/\/+$/, ""),
|
|
6105
|
+
audience: clientId,
|
|
6106
|
+
sessionCookieName,
|
|
6107
|
+
label: `authenticate-oidc (Issuer=${issuer})`
|
|
6108
|
+
};
|
|
6109
|
+
}
|
|
5835
6110
|
|
|
5836
6111
|
//#endregion
|
|
5837
6112
|
//#region src/cli/commands/local-start-alb.ts
|
|
@@ -5973,6 +6248,7 @@ function albStrategy(options) {
|
|
|
5973
6248
|
hostPort,
|
|
5974
6249
|
protocol: listener.listenerProtocol,
|
|
5975
6250
|
...listener.defaultAction ? { defaultAction: qualify(listener.defaultAction) } : {},
|
|
6251
|
+
...listener.defaultAuthGuard ? { defaultAuthGuard: listener.defaultAuthGuard } : {},
|
|
5976
6252
|
rules: listener.rules.map((r) => ({
|
|
5977
6253
|
priority: r.priority,
|
|
5978
6254
|
pathPatterns: r.pathPatterns,
|
|
@@ -5981,7 +6257,8 @@ function albStrategy(options) {
|
|
|
5981
6257
|
httpRequestMethods: r.httpRequestMethods,
|
|
5982
6258
|
queryStringConditions: r.queryStringConditions,
|
|
5983
6259
|
sourceIpCidrs: r.sourceIpCidrs,
|
|
5984
|
-
action: qualify(r.action)
|
|
6260
|
+
action: qualify(r.action),
|
|
6261
|
+
...r.authGuard ? { authGuard: r.authGuard } : {}
|
|
5985
6262
|
}))
|
|
5986
6263
|
});
|
|
5987
6264
|
}
|
|
@@ -6010,7 +6287,7 @@ function albStrategy(options) {
|
|
|
6010
6287
|
*/
|
|
6011
6288
|
function createLocalStartAlbCommand(opts = {}) {
|
|
6012
6289
|
setEmbedConfig(opts.embedConfig);
|
|
6013
|
-
return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners (TLS terminated locally with --tls-cert/--tls-key or an auto-generated self-signed cert); all six ALB rule-condition fields (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions
|
|
6290
|
+
return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners (TLS terminated locally with --tls-cert/--tls-key or an auto-generated self-signed cert); all six ALB rule-condition fields (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions enforce a local Bearer-JWT check (or AWSELBAuthSessionCookie pass-through) against the same JWKS / OIDC discovery URL the deployed ALB would; use --bearer-token <jwt> to inject a default token or --no-verify-auth to disable the guard. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Must be set together with --tls-key. Omit both flags to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).action(withErrorHandling(async (targets, options) => {
|
|
6014
6291
|
await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
|
|
6015
6292
|
})));
|
|
6016
6293
|
}
|
|
@@ -6087,4 +6364,4 @@ function createLocalListCommand(opts = {}) {
|
|
|
6087
6364
|
|
|
6088
6365
|
//#endregion
|
|
6089
6366
|
export { createLocalRunTaskCommand as a, createLocalStartServiceCommand as i, formatTargetListing as n, createLocalInvokeAgentCoreCommand as o, createLocalStartAlbCommand as r, createLocalInvokeCommand as s, createLocalListCommand as t };
|
|
6090
|
-
//# sourceMappingURL=local-list-
|
|
6367
|
+
//# sourceMappingURL=local-list-ChGfKSkV.js.map
|