cdk-local 0.49.0 → 0.51.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 +3 -3
- package/dist/cli.js +3 -3
- package/dist/{cloud-map-resolver-CW4Paz5K.js → cloud-map-resolver-BvhnCkSe.js} +36 -9
- package/dist/cloud-map-resolver-BvhnCkSe.js.map +1 -0
- package/dist/{error-handler-GsADCf-H.d.ts → error-handler-Chucd8C_.d.ts} +3 -2
- package/dist/error-handler-Chucd8C_.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 +1 -1
- package/dist/internal.js +1 -1
- package/dist/{local-list-BovxL7PC.js → local-list-DbBCVhla.js} +456 -55
- package/dist/local-list-DbBCVhla.js.map +1 -0
- package/package.json +1 -1
- package/dist/cloud-map-resolver-CW4Paz5K.js.map +0 -1
- package/dist/error-handler-GsADCf-H.d.ts.map +0 -1
- package/dist/local-list-BovxL7PC.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 { At as buildDockerImage, Bn as readCdkPathOrUndefined, Bt as streamLogs, Ct as getDockerImageBySourceHash, Dt as buildContainerImage, Et as architectureToPlatform, Fn as derivePseudoParametersFromRegion, Ft as execEnvForSecrets, Gn as withErrorHandling, Gt as TASK_ROLE_ACCOUNT_PLACEHOLDER, Hn as CdkLocalError, Ht as resolveRuntimeFileExtension, It as pickFreePort, Jn as commonOptions, Jt as derivePartitionAndUrlSuffix, Kn as applyRoleArnIfSet, Kt as applyCrossStackResolverToTask, Lt as pullImage, Mn as pickAgentCoreCandidateStack, Mt as SENSITIVE_ENV_KEYS, Nn as resolveAgentCoreTarget, Nt as appendEnvFlags, Ot as parseEcrUri, Pn as resolveLambdaTarget, Pt as ensureDockerAvailable, Q as verifyJwtViaDiscovery, Qn as warnIfDeprecatedRegion, Qt as applyDeployedEnvFallback, Rn as matchStacks, Rt as removeContainer, St as AssetManifestLoader, Tt as waitForRieReady, Un as LocalInvokeBuildError, Ut as resolveRuntimeImage, Vn as resolveCdkPathToLogicalIds, Vt as resolveRuntimeCodeMountPath, Wn as LocalStartServiceError, Wt as EcsTaskResolutionError, Xn as deprecatedRegionOption, Xt as parseEcsTarget, Y as createJwksCache, Yn as contextOptions, Yt as detectEcsImageResolutionNeeds, Zn as parseContextOptions, Zt as resolveEcsTaskTarget, _n as Synthesizer, at as invokeAgentCoreWs, bn as countTargets, bt as writeProfileCredentialsFile, c as resolveProfileCredentials$1, cn as rejectExplicitCfnStackWithMultipleStacks, ft as invokeAgentCore, gt as buildAgentCoreCodeImage, hn as resolveApp, i as getPublishedHostPort, in as materializeLayerFromArn, jt as DockerRunnerError, kn as AGENTCORE_MCP_PROTOCOL, kt as pullEcrImage, ln as resolveCfnFallbackRegion, lt as mcpInvokeOnce, mt as downloadAndExtractS3Bundle, n as CloudMapRegistry, nn as substituteEnvVarsFromStateAsync, on as createLocalStateProvider, ot as MCP_CONTAINER_PORT, pt as waitForAgentCorePing, qn as appOptions, qt as checkVolumeHostPath, r as getContainerNetworkIp, rn as resolveEnvVars, st as MCP_PATH, t as buildCloudMapIndex, vn as resolveMultiTarget, wt as invokeRie, xn as listTargets, xt as singleFlight, yn as resolveSingleTarget, zn as buildCdkPathIndex, zt as runDetached } from "./cloud-map-resolver-
|
|
2
|
+
import { At as buildDockerImage, Bn as readCdkPathOrUndefined, Bt as streamLogs, Ct as getDockerImageBySourceHash, Dt as buildContainerImage, Et as architectureToPlatform, Fn as derivePseudoParametersFromRegion, Ft as execEnvForSecrets, Gn as withErrorHandling, Gt as TASK_ROLE_ACCOUNT_PLACEHOLDER, Hn as CdkLocalError, Ht as resolveRuntimeFileExtension, It as pickFreePort, Jn as commonOptions, Jt as derivePartitionAndUrlSuffix, Kn as applyRoleArnIfSet, Kt as applyCrossStackResolverToTask, Lt as pullImage, Mn as pickAgentCoreCandidateStack, Mt as SENSITIVE_ENV_KEYS, Nn as resolveAgentCoreTarget, Nt as appendEnvFlags, Ot as parseEcrUri, Pn as resolveLambdaTarget, Pt as ensureDockerAvailable, Q as verifyJwtViaDiscovery, Qn as warnIfDeprecatedRegion, Qt as applyDeployedEnvFallback, Rn as matchStacks, Rt as removeContainer, St as AssetManifestLoader, Tt as waitForRieReady, Un as LocalInvokeBuildError, Ut as resolveRuntimeImage, Vn as resolveCdkPathToLogicalIds, Vt as resolveRuntimeCodeMountPath, Wn as LocalStartServiceError, Wt as EcsTaskResolutionError, Xn as deprecatedRegionOption, Xt as parseEcsTarget, Y as createJwksCache, Yn as contextOptions, Yt as detectEcsImageResolutionNeeds, Zn as parseContextOptions, Zt as resolveEcsTaskTarget, _n as Synthesizer, at as invokeAgentCoreWs, bn as countTargets, bt as writeProfileCredentialsFile, c as resolveProfileCredentials$1, cn as rejectExplicitCfnStackWithMultipleStacks, en as substituteAgainstStateAsync, ft as invokeAgentCore, gt as buildAgentCoreCodeImage, hn as resolveApp, i as getPublishedHostPort, in as materializeLayerFromArn, jt as DockerRunnerError, kn as AGENTCORE_MCP_PROTOCOL, kt as pullEcrImage, ln as resolveCfnFallbackRegion, lt as mcpInvokeOnce, mt as downloadAndExtractS3Bundle, n as CloudMapRegistry, nn as substituteEnvVarsFromStateAsync, on as createLocalStateProvider, ot as MCP_CONTAINER_PORT, pt as waitForAgentCorePing, qn as appOptions, qt as checkVolumeHostPath, r as getContainerNetworkIp, rn as resolveEnvVars, st as MCP_PATH, t as buildCloudMapIndex, vn as resolveMultiTarget, wt as invokeRie, xn as listTargets, xt as singleFlight, yn as resolveSingleTarget, zn as buildCdkPathIndex, zt as runDetached } from "./cloud-map-resolver-BvhnCkSe.js";
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
@@ -679,6 +679,7 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
679
679
|
if (isMcp) {
|
|
680
680
|
if (resolved.jwtAuthorizer || options.bearerToken) logger.info(`MCP runtime: invoking the local container's ${MCP_PATH} directly (vanilla MCP). An inbound JWT / --bearer-token is an AgentCore managed-plane concern and is not applied locally.`);
|
|
681
681
|
} else authorization = await resolveInboundAuthorization(resolved, options);
|
|
682
|
+
await resolveFromS3BucketIntrinsic(resolved, stateProvider, loadedState, imageContext);
|
|
682
683
|
const image = await resolveAgentCoreImage(resolved, options, loadedState);
|
|
683
684
|
const { env: dockerEnv, sensitiveEnvKeys } = await buildContainerEnv(resolved, options, profileCredentials, profileCredsFile, stateProvider, loadedState, imageContext);
|
|
684
685
|
const hostPort = await pickFreePort();
|
|
@@ -850,6 +851,12 @@ async function resolveAgentCoreCodeImage(resolved, code, options, architecture,
|
|
|
850
851
|
*/
|
|
851
852
|
async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options, architecture, loaded) {
|
|
852
853
|
const logger = getLogger();
|
|
854
|
+
if (typeof s3Source.bucket !== "string" || s3Source.bucket.length === 0) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' fromS3 bundle reached the image step with no literal bucket. This is a cdk-local bug — please report it.`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_UNRESOLVED");
|
|
855
|
+
const location = {
|
|
856
|
+
bucket: s3Source.bucket,
|
|
857
|
+
key: s3Source.key,
|
|
858
|
+
...s3Source.versionId !== void 0 && { versionId: s3Source.versionId }
|
|
859
|
+
};
|
|
853
860
|
const region = options.region ?? options.stackRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? resolved.stack.region;
|
|
854
861
|
const assumeRoleArn = resolveAssumeRoleArn(options, resolved, loaded);
|
|
855
862
|
let credentials;
|
|
@@ -858,7 +865,7 @@ async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options
|
|
|
858
865
|
} catch (err) {
|
|
859
866
|
logger.warn(`--assume-role: STS AssumeRole(${assumeRoleArn}) failed for the fromS3 bundle download: ${err instanceof Error ? err.message : String(err)}. Falling back to ${options.profile ? `--profile ${options.profile}` : "the default credentials"}.`);
|
|
860
867
|
}
|
|
861
|
-
const bundle = await downloadAndExtractS3Bundle(
|
|
868
|
+
const bundle = await downloadAndExtractS3Bundle(location, {
|
|
862
869
|
...region !== void 0 && { region },
|
|
863
870
|
...options.profile !== void 0 && { profile: options.profile },
|
|
864
871
|
...credentials !== void 0 && { credentials }
|
|
@@ -941,6 +948,46 @@ async function buildContainerEnv(resolved, options, profileCredentials, profileC
|
|
|
941
948
|
};
|
|
942
949
|
}
|
|
943
950
|
/**
|
|
951
|
+
* Resolve a fromS3 bundle's intrinsic `Code.S3.Bucket` to a literal bucket
|
|
952
|
+
* name in place on `resolved.codeArtifact.s3Source.bucket`. Uses the SAME
|
|
953
|
+
* state-substitution machinery env vars use under `--from-cfn-stack`, so
|
|
954
|
+
* every cross-stack intrinsic that path supports (`Ref` / `Fn::ImportValue` /
|
|
955
|
+
* `Fn::GetStackOutput`) is supported transparently here.
|
|
956
|
+
*
|
|
957
|
+
* No-op when there is no intrinsic to resolve. Errors when no state is
|
|
958
|
+
* available, or when the substitution returns a non-string / unresolved value.
|
|
959
|
+
*
|
|
960
|
+
* Exported so a unit test can drive the gate without the full Docker pipeline.
|
|
961
|
+
*/
|
|
962
|
+
async function resolveFromS3BucketIntrinsic(resolved, stateProvider, loaded, imageContext) {
|
|
963
|
+
const s3Source = resolved.codeArtifact?.s3Source;
|
|
964
|
+
if (!s3Source || s3Source.bucketIntrinsic === void 0) return;
|
|
965
|
+
if (s3Source.bucket !== void 0) return;
|
|
966
|
+
if (!stateProvider || !loaded) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' fromS3 bundle's Code.S3.Bucket is an unresolved intrinsic (${describeIntrinsic(s3Source.bucketIntrinsic)}). Pass --from-cfn-stack so its physical bucket name can be resolved against the deployed stack state.`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_INTRINSIC_NO_STATE");
|
|
967
|
+
const subContext = {
|
|
968
|
+
resources: imageContext?.stateResources ?? loaded.resources,
|
|
969
|
+
consumerRegion: loaded.region
|
|
970
|
+
};
|
|
971
|
+
const pseudo = imageContext?.pseudoParameters ?? derivePseudoParametersFromRegion(loaded.region);
|
|
972
|
+
if (pseudo) subContext.pseudoParameters = pseudo;
|
|
973
|
+
const crossStackResolver = await stateProvider.buildCrossStackResolver(loaded.region);
|
|
974
|
+
if (crossStackResolver) subContext.crossStackResolver = crossStackResolver;
|
|
975
|
+
const result = await substituteAgainstStateAsync(s3Source.bucketIntrinsic, subContext);
|
|
976
|
+
if (result.kind !== "literal") throw new CdkLocalError(`Could not resolve AgentCore Runtime '${resolved.logicalId}' fromS3 Code.S3.Bucket intrinsic (${describeIntrinsic(s3Source.bucketIntrinsic)}) against the --from-cfn-stack state: ${result.reason}. Confirm the referenced resource / export exists in the deployed stack.`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_INTRINSIC_UNRESOLVED");
|
|
977
|
+
if (typeof result.value !== "string" || result.value.length === 0) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' fromS3 Code.S3.Bucket intrinsic resolved to a ${typeof result.value} value, not a bucket name string. (${describeIntrinsic(s3Source.bucketIntrinsic)})`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_INTRINSIC_NOT_STRING");
|
|
978
|
+
s3Source.bucket = result.value;
|
|
979
|
+
getLogger().info(`Resolved fromS3 Code.S3.Bucket from state: ${describeIntrinsic(s3Source.bucketIntrinsic)} -> ${result.value}`);
|
|
980
|
+
}
|
|
981
|
+
/** Render the intrinsic key for an error / log message (e.g. `Ref:Bucket1`). */
|
|
982
|
+
function describeIntrinsic(value) {
|
|
983
|
+
if (!value || typeof value !== "object") return String(value);
|
|
984
|
+
const obj = value;
|
|
985
|
+
const key = Object.keys(obj)[0] ?? "?";
|
|
986
|
+
const arg = obj[key];
|
|
987
|
+
if (typeof arg === "string") return `${key}:${arg}`;
|
|
988
|
+
return key;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
944
991
|
* Build the `--from-cfn-stack` image-resolution context + return the loaded
|
|
945
992
|
* state record (loaded once, reused by env substitution + role resolution).
|
|
946
993
|
* Mirrors `run-task`'s `buildEcsImageResolutionContext`: pseudo parameters
|
|
@@ -3623,7 +3670,10 @@ function handleProxyRequest(req, res, opts) {
|
|
|
3623
3670
|
if (opts.route) {
|
|
3624
3671
|
const action = opts.route({
|
|
3625
3672
|
path: url,
|
|
3626
|
-
...hostHeader(req)
|
|
3673
|
+
...hostHeader(req),
|
|
3674
|
+
headers: req.headers,
|
|
3675
|
+
...req.method !== void 0 && { method: req.method },
|
|
3676
|
+
...req.socket.remoteAddress !== void 0 && { sourceIp: req.socket.remoteAddress }
|
|
3627
3677
|
});
|
|
3628
3678
|
if (!action) return reply404(req, res, opts);
|
|
3629
3679
|
if (action.kind === "redirect" || action.kind === "fixed-response") {
|
|
@@ -3902,25 +3952,24 @@ function writeError(res, statusCode, message) {
|
|
|
3902
3952
|
* evaluated in ascending priority; the input order is irrelevant.
|
|
3903
3953
|
*
|
|
3904
3954
|
* Accepts either a request facts object or a bare path string (the path-only
|
|
3905
|
-
* form keeps the original signature working for callers that have no Host
|
|
3955
|
+
* form keeps the original signature working for callers that have no Host /
|
|
3956
|
+
* headers / method / source IP).
|
|
3906
3957
|
*/
|
|
3907
3958
|
function matchAlbPathRule(req, rules) {
|
|
3908
|
-
const
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
const requestPath = pathOf(path);
|
|
3913
|
-
const requestHost = host !== void 0 ? hostOf(host) : void 0;
|
|
3959
|
+
const facts = typeof req === "string" ? { path: req } : req;
|
|
3960
|
+
const requestPath = pathOf(facts.path);
|
|
3961
|
+
const requestQuery = queryOf(facts.path);
|
|
3962
|
+
const requestHost = facts.host !== void 0 ? hostOf(facts.host) : void 0;
|
|
3914
3963
|
const ordered = [...rules].sort((a, b) => a.priority - b.priority);
|
|
3915
|
-
for (const rule of ordered) if (ruleMatches(rule, requestPath, requestHost)) return rule.target;
|
|
3964
|
+
for (const rule of ordered) if (ruleMatches(rule, requestPath, requestQuery, requestHost, facts)) return rule.target;
|
|
3916
3965
|
}
|
|
3917
3966
|
/**
|
|
3918
|
-
* Whether a single rule's conditions all match.
|
|
3919
|
-
*
|
|
3920
|
-
*
|
|
3921
|
-
*
|
|
3967
|
+
* Whether a single rule's conditions all match. Each present condition field
|
|
3968
|
+
* is OR-matched within (multiple values) and AND'd across fields. An empty /
|
|
3969
|
+
* absent condition list means "no constraint on that field" (the condition was
|
|
3970
|
+
* absent on the rule).
|
|
3922
3971
|
*/
|
|
3923
|
-
function ruleMatches(rule, requestPath, requestHost) {
|
|
3972
|
+
function ruleMatches(rule, requestPath, requestQuery, requestHost, facts) {
|
|
3924
3973
|
if (rule.pathPatterns.length > 0) {
|
|
3925
3974
|
if (!rule.pathPatterns.some((pattern) => globToRegExp(pattern, false).test(requestPath))) return false;
|
|
3926
3975
|
}
|
|
@@ -3929,8 +3978,71 @@ function ruleMatches(rule, requestPath, requestHost) {
|
|
|
3929
3978
|
if (requestHost === void 0) return false;
|
|
3930
3979
|
if (!hostPatterns.some((pattern) => globToRegExp(pattern, true).test(requestHost))) return false;
|
|
3931
3980
|
}
|
|
3981
|
+
const headerConditions = rule.httpHeaderConditions ?? [];
|
|
3982
|
+
if (headerConditions.length > 0) {
|
|
3983
|
+
if (!headerConditions.every((cond) => httpHeaderConditionMatches(cond, facts.headers))) return false;
|
|
3984
|
+
}
|
|
3985
|
+
const methods = rule.httpRequestMethods ?? [];
|
|
3986
|
+
if (methods.length > 0) {
|
|
3987
|
+
if (facts.method === void 0) return false;
|
|
3988
|
+
if (!methods.includes(facts.method)) return false;
|
|
3989
|
+
}
|
|
3990
|
+
const queryConditions = rule.queryStringConditions ?? [];
|
|
3991
|
+
if (queryConditions.length > 0) {
|
|
3992
|
+
const params = parseQueryParams(requestQuery);
|
|
3993
|
+
if (!queryConditions.some((cond) => queryStringConditionMatches(cond, params))) return false;
|
|
3994
|
+
}
|
|
3995
|
+
const cidrs = rule.sourceIpCidrs ?? [];
|
|
3996
|
+
if (cidrs.length > 0) {
|
|
3997
|
+
if (facts.sourceIp === void 0) return false;
|
|
3998
|
+
const ip = unmapV4MappedV6(facts.sourceIp);
|
|
3999
|
+
if (!cidrs.some((cidr) => albCidrMatches(cidr, ip))) return false;
|
|
4000
|
+
}
|
|
3932
4001
|
return true;
|
|
3933
4002
|
}
|
|
4003
|
+
/**
|
|
4004
|
+
* Whether an `http-header` condition matches the request's headers. The
|
|
4005
|
+
* header name is looked up case-insensitively; each listed value glob is
|
|
4006
|
+
* matched case-insensitively against every value of that header (multi-valued
|
|
4007
|
+
* headers count each value separately).
|
|
4008
|
+
*/
|
|
4009
|
+
function httpHeaderConditionMatches(cond, headers) {
|
|
4010
|
+
if (!headers) return false;
|
|
4011
|
+
const rawValue = headerLookup(headers, cond.name.toLowerCase());
|
|
4012
|
+
if (rawValue === void 0) return false;
|
|
4013
|
+
const headerValues = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
4014
|
+
return cond.values.some((glob) => {
|
|
4015
|
+
const re = globToRegExp(glob, true);
|
|
4016
|
+
return headerValues.some((v) => re.test(v.toLowerCase()));
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
/**
|
|
4020
|
+
* Whether a single `query-string` condition `{ Key?, Value }` matches the
|
|
4021
|
+
* request's parsed query parameters. Both Key and Value are case-insensitive
|
|
4022
|
+
* `*` / `?` globs; when `Key` is absent, ANY parameter whose value matches the
|
|
4023
|
+
* Value glob satisfies the condition.
|
|
4024
|
+
*/
|
|
4025
|
+
function queryStringConditionMatches(cond, params) {
|
|
4026
|
+
const valueRe = globToRegExp(cond.value, true);
|
|
4027
|
+
const keyRe = cond.key !== void 0 ? globToRegExp(cond.key, true) : void 0;
|
|
4028
|
+
return params.some((p) => {
|
|
4029
|
+
if (keyRe !== void 0 && !keyRe.test(p.key.toLowerCase())) return false;
|
|
4030
|
+
return valueRe.test(p.value.toLowerCase());
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
/**
|
|
4034
|
+
* Whether an IPv4 or IPv6 address falls inside an IPv4 or IPv6 CIDR. Returns
|
|
4035
|
+
* `false` for mismatched families (an IPv4 address tested against an IPv6
|
|
4036
|
+
* CIDR, or vice versa) and for unparseable input.
|
|
4037
|
+
*/
|
|
4038
|
+
function albCidrMatches(cidr, ip) {
|
|
4039
|
+
const parsed = parseCidr(cidr);
|
|
4040
|
+
if (!parsed) return false;
|
|
4041
|
+
const addr = parseIpAddress(ip);
|
|
4042
|
+
if (!addr) return false;
|
|
4043
|
+
if (parsed.family !== addr.family) return false;
|
|
4044
|
+
return matchBitPrefix(parsed.bytes, addr.bytes, parsed.prefixLength);
|
|
4045
|
+
}
|
|
3934
4046
|
/** Strip the query string / fragment so only the URL path is matched. */
|
|
3935
4047
|
function pathOf(url) {
|
|
3936
4048
|
let end = url.length;
|
|
@@ -3941,6 +4053,45 @@ function pathOf(url) {
|
|
|
3941
4053
|
return url.slice(0, end);
|
|
3942
4054
|
}
|
|
3943
4055
|
/**
|
|
4056
|
+
* Pull the raw query string (`a=1&b=2`) out of a URL. Returns the empty string
|
|
4057
|
+
* when the URL has no `?`. The fragment is dropped.
|
|
4058
|
+
*/
|
|
4059
|
+
function queryOf(url) {
|
|
4060
|
+
const q = url.indexOf("?");
|
|
4061
|
+
if (q === -1) return "";
|
|
4062
|
+
const rest = url.slice(q + 1);
|
|
4063
|
+
const h = rest.indexOf("#");
|
|
4064
|
+
return h === -1 ? rest : rest.slice(0, h);
|
|
4065
|
+
}
|
|
4066
|
+
/**
|
|
4067
|
+
* Decode a raw query string into a flat list of `{ key, value }` entries
|
|
4068
|
+
* (preserving repeats), URI-decoding both sides and treating `+` as a space
|
|
4069
|
+
* (the `application/x-www-form-urlencoded` convention browsers use). Pairs
|
|
4070
|
+
* with no `=` are treated as `{ key, value: '' }`.
|
|
4071
|
+
*/
|
|
4072
|
+
function parseQueryParams(query) {
|
|
4073
|
+
if (!query) return [];
|
|
4074
|
+
const out = [];
|
|
4075
|
+
for (const pair of query.split("&")) {
|
|
4076
|
+
if (!pair) continue;
|
|
4077
|
+
const eq = pair.indexOf("=");
|
|
4078
|
+
const rawKey = eq === -1 ? pair : pair.slice(0, eq);
|
|
4079
|
+
const rawValue = eq === -1 ? "" : pair.slice(eq + 1);
|
|
4080
|
+
out.push({
|
|
4081
|
+
key: decodeQueryPart(rawKey),
|
|
4082
|
+
value: decodeQueryPart(rawValue)
|
|
4083
|
+
});
|
|
4084
|
+
}
|
|
4085
|
+
return out;
|
|
4086
|
+
}
|
|
4087
|
+
function decodeQueryPart(s) {
|
|
4088
|
+
try {
|
|
4089
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
4090
|
+
} catch {
|
|
4091
|
+
return s;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
/**
|
|
3944
4095
|
* Normalize a `Host` header for matching: drop any `:port` suffix and lower-case
|
|
3945
4096
|
* it (DNS hostnames are case-insensitive). IPv6 literals (`[::1]:8080`) keep the
|
|
3946
4097
|
* bracketed address and only the trailing port is removed.
|
|
@@ -3955,13 +4106,18 @@ function hostOf(host) {
|
|
|
3955
4106
|
const colon = trimmed.indexOf(":");
|
|
3956
4107
|
return (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();
|
|
3957
4108
|
}
|
|
4109
|
+
/** Case-insensitive header lookup against a Node-style headers dict. */
|
|
4110
|
+
function headerLookup(headers, lowerCaseName) {
|
|
4111
|
+
const direct = headers[lowerCaseName];
|
|
4112
|
+
if (direct !== void 0) return direct;
|
|
4113
|
+
for (const key of Object.keys(headers)) if (key.toLowerCase() === lowerCaseName) return headers[key];
|
|
4114
|
+
}
|
|
3958
4115
|
const REGEXP_META = /[.+^${}()|[\]\\]/;
|
|
3959
4116
|
/**
|
|
3960
4117
|
* Translate an ALB `*` / `?` glob into an anchored RegExp: `*` -> `.*`,
|
|
3961
|
-
* `?` -> `.`, every other character is escaped and matched literally.
|
|
3962
|
-
*
|
|
3963
|
-
* lower-cased
|
|
3964
|
-
* case-sensitive.
|
|
4118
|
+
* `?` -> `.`, every other character is escaped and matched literally. The
|
|
4119
|
+
* `caseInsensitive` form lower-cases the pattern (callers pair it with a
|
|
4120
|
+
* lower-cased input); path patterns are case-sensitive.
|
|
3965
4121
|
*/
|
|
3966
4122
|
function globToRegExp(pattern, caseInsensitive) {
|
|
3967
4123
|
const source = caseInsensitive ? pattern.toLowerCase() : pattern;
|
|
@@ -3972,6 +4128,131 @@ function globToRegExp(pattern, caseInsensitive) {
|
|
|
3972
4128
|
else body += ch;
|
|
3973
4129
|
return new RegExp(`^${body}$`);
|
|
3974
4130
|
}
|
|
4131
|
+
/** Parse a CIDR (`ip/prefix`) into its address bytes + prefix length. */
|
|
4132
|
+
function parseCidr(cidr) {
|
|
4133
|
+
const slash = cidr.indexOf("/");
|
|
4134
|
+
if (slash === -1) return void 0;
|
|
4135
|
+
const addrPart = cidr.slice(0, slash);
|
|
4136
|
+
const prefixPart = cidr.slice(slash + 1);
|
|
4137
|
+
if (!/^\d+$/.test(prefixPart)) return void 0;
|
|
4138
|
+
const prefixLength = parseInt(prefixPart, 10);
|
|
4139
|
+
const addr = parseIpAddress(addrPart);
|
|
4140
|
+
if (!addr) return void 0;
|
|
4141
|
+
const maxPrefix = addr.family === "v4" ? 32 : 128;
|
|
4142
|
+
if (prefixLength < 0 || prefixLength > maxPrefix) return void 0;
|
|
4143
|
+
return {
|
|
4144
|
+
family: addr.family,
|
|
4145
|
+
bytes: addr.bytes,
|
|
4146
|
+
prefixLength
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
/** Parse an IPv4 (`1.2.3.4`) or IPv6 address into its byte representation. */
|
|
4150
|
+
function parseIpAddress(ip) {
|
|
4151
|
+
if (ip.includes(".") && !ip.includes(":")) return parseIpv4(ip);
|
|
4152
|
+
if (ip.includes(":")) return parseIpv6(ip);
|
|
4153
|
+
}
|
|
4154
|
+
function parseIpv4(ip) {
|
|
4155
|
+
const parts = ip.split(".");
|
|
4156
|
+
if (parts.length !== 4) return void 0;
|
|
4157
|
+
const bytes = new Uint8Array(4);
|
|
4158
|
+
for (let i = 0; i < 4; i++) {
|
|
4159
|
+
const part = parts[i];
|
|
4160
|
+
if (!/^\d+$/.test(part)) return void 0;
|
|
4161
|
+
const n = parseInt(part, 10);
|
|
4162
|
+
if (n < 0 || n > 255) return void 0;
|
|
4163
|
+
bytes[i] = n;
|
|
4164
|
+
}
|
|
4165
|
+
return {
|
|
4166
|
+
family: "v4",
|
|
4167
|
+
bytes
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Parse a textual IPv6 address (incl. `::` shorthand and IPv4-suffix form like
|
|
4172
|
+
* `::ffff:1.2.3.4`) into its 16-byte representation.
|
|
4173
|
+
*/
|
|
4174
|
+
function parseIpv6(ip) {
|
|
4175
|
+
let s = ip;
|
|
4176
|
+
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
4177
|
+
let v4Suffix;
|
|
4178
|
+
const lastColon = s.lastIndexOf(":");
|
|
4179
|
+
if (lastColon !== -1 && s.slice(lastColon + 1).includes(".")) {
|
|
4180
|
+
const parsed = parseIpv4(s.slice(lastColon + 1));
|
|
4181
|
+
if (!parsed) return void 0;
|
|
4182
|
+
v4Suffix = parsed.bytes;
|
|
4183
|
+
s = s.slice(0, lastColon) + ":0:0";
|
|
4184
|
+
}
|
|
4185
|
+
const doubleColon = s.indexOf("::");
|
|
4186
|
+
let head;
|
|
4187
|
+
let tail;
|
|
4188
|
+
if (doubleColon === -1) {
|
|
4189
|
+
head = s.split(":");
|
|
4190
|
+
tail = [];
|
|
4191
|
+
} else {
|
|
4192
|
+
head = s.slice(0, doubleColon) === "" ? [] : s.slice(0, doubleColon).split(":");
|
|
4193
|
+
tail = s.slice(doubleColon + 2) === "" ? [] : s.slice(doubleColon + 2).split(":");
|
|
4194
|
+
}
|
|
4195
|
+
if (head.length + tail.length > 8) return void 0;
|
|
4196
|
+
if (doubleColon === -1 && head.length !== 8) return void 0;
|
|
4197
|
+
const groups = new Array(8).fill(0);
|
|
4198
|
+
for (let i = 0; i < head.length; i++) {
|
|
4199
|
+
const g = parseHexGroup(head[i]);
|
|
4200
|
+
if (g === void 0) return void 0;
|
|
4201
|
+
groups[i] = g;
|
|
4202
|
+
}
|
|
4203
|
+
for (let i = 0; i < tail.length; i++) {
|
|
4204
|
+
const g = parseHexGroup(tail[tail.length - 1 - i]);
|
|
4205
|
+
if (g === void 0) return void 0;
|
|
4206
|
+
groups[7 - i] = g;
|
|
4207
|
+
}
|
|
4208
|
+
const bytes = new Uint8Array(16);
|
|
4209
|
+
for (let i = 0; i < 8; i++) {
|
|
4210
|
+
bytes[i * 2] = groups[i] >> 8 & 255;
|
|
4211
|
+
bytes[i * 2 + 1] = groups[i] & 255;
|
|
4212
|
+
}
|
|
4213
|
+
if (v4Suffix) {
|
|
4214
|
+
bytes[12] = v4Suffix[0];
|
|
4215
|
+
bytes[13] = v4Suffix[1];
|
|
4216
|
+
bytes[14] = v4Suffix[2];
|
|
4217
|
+
bytes[15] = v4Suffix[3];
|
|
4218
|
+
}
|
|
4219
|
+
return {
|
|
4220
|
+
family: "v6",
|
|
4221
|
+
bytes
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
function parseHexGroup(g) {
|
|
4225
|
+
if (g.length === 0 || g.length > 4) return void 0;
|
|
4226
|
+
if (!/^[0-9a-fA-F]+$/.test(g)) return void 0;
|
|
4227
|
+
return parseInt(g, 16);
|
|
4228
|
+
}
|
|
4229
|
+
/**
|
|
4230
|
+
* Unmap an IPv4-mapped IPv6 source IP (`::ffff:a.b.c.d` or `::ffff:NNNN:NNNN`)
|
|
4231
|
+
* to its bare IPv4 form. Node reports `::ffff:127.0.0.1` for an IPv4 client on
|
|
4232
|
+
* a dual-stack listener; rules that name a v4 CIDR should still match.
|
|
4233
|
+
*/
|
|
4234
|
+
function unmapV4MappedV6(ip) {
|
|
4235
|
+
const m = /^::ffff:(.+)$/i.exec(ip);
|
|
4236
|
+
if (!m) return ip;
|
|
4237
|
+
const suffix = m[1];
|
|
4238
|
+
if (suffix.includes(".")) return suffix;
|
|
4239
|
+
const parts = suffix.split(":");
|
|
4240
|
+
if (parts.length !== 2) return ip;
|
|
4241
|
+
const high = parseInt(parts[0], 16);
|
|
4242
|
+
const low = parseInt(parts[1], 16);
|
|
4243
|
+
if (!Number.isFinite(high) || !Number.isFinite(low)) return ip;
|
|
4244
|
+
return `${high >> 8 & 255}.${high & 255}.${low >> 8 & 255}.${low & 255}`;
|
|
4245
|
+
}
|
|
4246
|
+
/** Whether the first `prefixLength` bits of `a` equal those of `b`. */
|
|
4247
|
+
function matchBitPrefix(a, b, prefixLength) {
|
|
4248
|
+
if (a.length !== b.length) return false;
|
|
4249
|
+
const fullBytes = Math.floor(prefixLength / 8);
|
|
4250
|
+
for (let i = 0; i < fullBytes; i++) if (a[i] !== b[i]) return false;
|
|
4251
|
+
const remainingBits = prefixLength % 8;
|
|
4252
|
+
if (remainingBits === 0) return true;
|
|
4253
|
+
const mask = 255 << 8 - remainingBits;
|
|
4254
|
+
return (a[fullBytes] & mask) === (b[fullBytes] & mask);
|
|
4255
|
+
}
|
|
3975
4256
|
|
|
3976
4257
|
//#endregion
|
|
3977
4258
|
//#region src/local/front-door-lambda-runner.ts
|
|
@@ -4464,6 +4745,10 @@ async function buildFrontDoor(plan, options, logger) {
|
|
|
4464
4745
|
priority: r.priority,
|
|
4465
4746
|
pathPatterns: r.pathPatterns,
|
|
4466
4747
|
hostPatterns: r.hostPatterns,
|
|
4748
|
+
httpHeaderConditions: r.httpHeaderConditions,
|
|
4749
|
+
httpRequestMethods: r.httpRequestMethods,
|
|
4750
|
+
queryStringConditions: r.queryStringConditions,
|
|
4751
|
+
sourceIpCidrs: r.sourceIpCidrs,
|
|
4467
4752
|
target: toRouteAction(r.action)
|
|
4468
4753
|
}));
|
|
4469
4754
|
const route = (req) => matchAlbPathRule(req, ruleRoutes) ?? defaultRoute;
|
|
@@ -4505,13 +4790,20 @@ async function buildFrontDoor(plan, options, logger) {
|
|
|
4505
4790
|
lambdaRunners: [...lambdaRegistry.values()]
|
|
4506
4791
|
};
|
|
4507
4792
|
}
|
|
4508
|
-
/** Human-readable summary of a planned rule's
|
|
4793
|
+
/** Human-readable summary of a planned rule's six ALB condition fields (for the boot banner). */
|
|
4509
4794
|
function describeConditions(rule) {
|
|
4510
4795
|
const parts = [];
|
|
4511
4796
|
if (rule.pathPatterns.length > 0) parts.push(`path ${rule.pathPatterns.join(", ")}`);
|
|
4512
4797
|
if (rule.hostPatterns.length > 0) parts.push(`host ${rule.hostPatterns.join(", ")}`);
|
|
4798
|
+
for (const h of rule.httpHeaderConditions) parts.push(`header ${h.name}: ${h.values.join(", ")}`);
|
|
4799
|
+
if (rule.httpRequestMethods.length > 0) parts.push(`method ${rule.httpRequestMethods.join(", ")}`);
|
|
4800
|
+
if (rule.queryStringConditions.length > 0) parts.push(`query ${rule.queryStringConditions.map(describeQueryStringCondition).join(", ")}`);
|
|
4801
|
+
if (rule.sourceIpCidrs.length > 0) parts.push(`source-ip ${rule.sourceIpCidrs.join(", ")}`);
|
|
4513
4802
|
return parts.join(" AND ") || "(no condition)";
|
|
4514
4803
|
}
|
|
4804
|
+
function describeQueryStringCondition(c) {
|
|
4805
|
+
return c.key !== void 0 ? `${c.key}=${c.value}` : c.value;
|
|
4806
|
+
}
|
|
4515
4807
|
/** Human-readable summary of a planned action (for the boot banner). */
|
|
4516
4808
|
function describeAction(action) {
|
|
4517
4809
|
if (action.kind === "redirect") return `redirect ${action.statusCode}`;
|
|
@@ -4740,7 +5032,11 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
4740
5032
|
* DefaultActions:[ <action> ] }
|
|
4741
5033
|
* ElasticLoadBalancingV2::ListenerRule : { ListenerArn:{Ref:<Listener>}, Priority,
|
|
4742
5034
|
* Conditions:[{ Field:"path-pattern", PathPatternConfig:{ Values:["/api/*"] } },
|
|
4743
|
-
* { Field:"host-header", HostHeaderConfig:{ Values:["api.example.com"] } }
|
|
5035
|
+
* { Field:"host-header", HostHeaderConfig:{ Values:["api.example.com"] } },
|
|
5036
|
+
* { Field:"http-header", HttpHeaderConfig:{ HttpHeaderName:"X-API", Values:["*"] } },
|
|
5037
|
+
* { Field:"http-request-method", HttpRequestMethodConfig:{ Values:["POST"] } },
|
|
5038
|
+
* { Field:"query-string", QueryStringConfig:{ Values:[{ Key:"v", Value:"1" }] } },
|
|
5039
|
+
* { Field:"source-ip", SourceIpConfig:{ Values:["10.0.0.0/8"] } }],
|
|
4744
5040
|
* Actions:[ <action> ] }
|
|
4745
5041
|
* ElasticLoadBalancingV2::TargetGroup : { Port, Protocol, TargetType:"ip" }
|
|
4746
5042
|
* ECS::Service.LoadBalancers[] -> { ContainerName, ContainerPort, TargetGroupArn:{Ref:<TG>} }
|
|
@@ -4758,16 +5054,16 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
4758
5054
|
* `LoadBalancers[]` references each target group (a reverse scan; there is no
|
|
4759
5055
|
* direct TG -> service pointer). Output is a per-listener routing table: a
|
|
4760
5056
|
* default action plus the ordered rules, each carrying its resolved action and
|
|
4761
|
-
* its
|
|
5057
|
+
* its full set of routing conditions.
|
|
4762
5058
|
*
|
|
4763
|
-
* Scope: HTTP listeners;
|
|
4764
|
-
*
|
|
4765
|
-
*
|
|
5059
|
+
* Scope: HTTP listeners; all six ALB rule-condition fields (`path-pattern`,
|
|
5060
|
+
* `host-header`, `http-header`, `http-request-method`, `query-string`,
|
|
5061
|
+
* `source-ip`); `forward` (single or weighted) to ECS services AND/OR Lambda
|
|
5062
|
+
* functions (`TargetType:"lambda"` target groups — #123: the TG -> backing
|
|
4766
5063
|
* `AWS::Lambda::Function` is resolved and the front-door invokes it locally per
|
|
4767
5064
|
* request); `redirect` / `fixed-response` actions. A single weighted forward may
|
|
4768
|
-
* mix ECS and Lambda targets. Skipped with a warning: HTTPS/TLS listeners
|
|
4769
|
-
*
|
|
4770
|
-
* source-ip), and `authenticate-cognito` / `authenticate-oidc` actions.
|
|
5065
|
+
* mix ECS and Lambda targets. Skipped with a warning: HTTPS/TLS listeners and
|
|
5066
|
+
* `authenticate-cognito` / `authenticate-oidc` actions.
|
|
4771
5067
|
*/
|
|
4772
5068
|
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
4773
5069
|
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
@@ -4803,18 +5099,20 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
4803
5099
|
for (const { ruleLogicalId, ruleProps } of rulesByListener.get(listenerLogicalId) ?? []) {
|
|
4804
5100
|
const priority = parsePriority(ruleProps["Priority"]);
|
|
4805
5101
|
const ruleLabel = `Listener rule '${ruleLogicalId}' (priority ${priority})`;
|
|
4806
|
-
const
|
|
4807
|
-
if (
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
}
|
|
4811
|
-
if (pathPatterns.length === 0 && hostPatterns.length === 0) continue;
|
|
5102
|
+
const parsed = parseRuleConditions(ruleProps["Conditions"], ruleLabel, warnings);
|
|
5103
|
+
if (!parsed) continue;
|
|
5104
|
+
const { pathPatterns, hostPatterns, httpHeaderConditions, httpRequestMethods, queryStringConditions, sourceIpCidrs } = parsed;
|
|
5105
|
+
if (!(pathPatterns.length > 0 || hostPatterns.length > 0 || httpHeaderConditions.length > 0 || httpRequestMethods.length > 0 || queryStringConditions.length > 0 || sourceIpCidrs.length > 0)) continue;
|
|
4812
5106
|
const action = resolveAction(ruleProps["Actions"], resources, tgToService, stackName, `${ruleLabel} action`, warnings);
|
|
4813
5107
|
if (!action) continue;
|
|
4814
5108
|
rules.push({
|
|
4815
5109
|
priority,
|
|
4816
5110
|
pathPatterns,
|
|
4817
5111
|
hostPatterns,
|
|
5112
|
+
httpHeaderConditions,
|
|
5113
|
+
httpRequestMethods,
|
|
5114
|
+
queryStringConditions,
|
|
5115
|
+
sourceIpCidrs,
|
|
4818
5116
|
action
|
|
4819
5117
|
});
|
|
4820
5118
|
}
|
|
@@ -5047,36 +5345,135 @@ function indexRulesByListener(resources) {
|
|
|
5047
5345
|
return index;
|
|
5048
5346
|
}
|
|
5049
5347
|
/**
|
|
5050
|
-
* Parse a ListenerRule's `Conditions` into
|
|
5051
|
-
* `
|
|
5052
|
-
*
|
|
5053
|
-
*
|
|
5054
|
-
*
|
|
5348
|
+
* Parse a ListenerRule's `Conditions` into the six supported fields. Returns
|
|
5349
|
+
* `undefined` when an unknown field is encountered or a known field is
|
|
5350
|
+
* malformed enough that honoring the rule would silently drop a constraint
|
|
5351
|
+
* — both surface a warning and skip the whole rule (ALB ANDs conditions of
|
|
5352
|
+
* different fields, so dropping any condition would route requests it should
|
|
5353
|
+
* not).
|
|
5354
|
+
*
|
|
5355
|
+
* Multiple `http-header` conditions on different names are kept (they AND).
|
|
5356
|
+
* Multiple `path-pattern` / `host-header` / `http-request-method` /
|
|
5357
|
+
* `query-string` / `source-ip` conditions on the same field merge their
|
|
5358
|
+
* values (each field OR-matches). A `source-ip` value that does not parse as
|
|
5359
|
+
* a CIDR makes the whole rule unsupported (the rule's authoring intent was
|
|
5360
|
+
* specific; dropping the invalid range would silently widen the allow list).
|
|
5055
5361
|
*/
|
|
5056
|
-
function parseRuleConditions(conditions) {
|
|
5057
|
-
const
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5362
|
+
function parseRuleConditions(conditions, ruleLabel, warnings) {
|
|
5363
|
+
const out = {
|
|
5364
|
+
pathPatterns: [],
|
|
5365
|
+
hostPatterns: [],
|
|
5366
|
+
httpHeaderConditions: [],
|
|
5367
|
+
httpRequestMethods: [],
|
|
5368
|
+
queryStringConditions: [],
|
|
5369
|
+
sourceIpCidrs: []
|
|
5064
5370
|
};
|
|
5371
|
+
if (!Array.isArray(conditions)) return out;
|
|
5065
5372
|
for (const cond of conditions) {
|
|
5066
5373
|
if (!cond || typeof cond !== "object") continue;
|
|
5067
5374
|
const c = cond;
|
|
5068
5375
|
const field = typeof c["Field"] === "string" ? c["Field"] : "(unknown)";
|
|
5069
|
-
if (field === "path-pattern") pathPatterns.push(...conditionValues(c, "PathPatternConfig"));
|
|
5070
|
-
else if (field === "host-header") hostPatterns.push(...conditionValues(c, "HostHeaderConfig"));
|
|
5071
|
-
else
|
|
5376
|
+
if (field === "path-pattern") out.pathPatterns.push(...conditionValues(c, "PathPatternConfig"));
|
|
5377
|
+
else if (field === "host-header") out.hostPatterns.push(...conditionValues(c, "HostHeaderConfig"));
|
|
5378
|
+
else if (field === "http-header") {
|
|
5379
|
+
const parsed = parseHttpHeaderCondition(c);
|
|
5380
|
+
if (!parsed) {
|
|
5381
|
+
warnings.push(`${ruleLabel} has an http-header condition with no HttpHeaderName / Values; skipping the rule.`);
|
|
5382
|
+
return;
|
|
5383
|
+
}
|
|
5384
|
+
out.httpHeaderConditions.push(parsed);
|
|
5385
|
+
} else if (field === "http-request-method") out.httpRequestMethods.push(...conditionValues(c, "HttpRequestMethodConfig"));
|
|
5386
|
+
else if (field === "query-string") {
|
|
5387
|
+
const { parsed, hadEntries } = parseQueryStringConditionValues(c);
|
|
5388
|
+
if (hadEntries && parsed.length === 0) {
|
|
5389
|
+
warnings.push(`${ruleLabel} query-string condition has no parseable { Key?, Value } entries; skipping the rule.`);
|
|
5390
|
+
return;
|
|
5391
|
+
}
|
|
5392
|
+
out.queryStringConditions.push(...parsed);
|
|
5393
|
+
} else if (field === "source-ip") {
|
|
5394
|
+
const values = conditionValues(c, "SourceIpConfig");
|
|
5395
|
+
const invalid = values.filter((v) => !isValidCidr(v));
|
|
5396
|
+
if (invalid.length > 0) {
|
|
5397
|
+
warnings.push(`${ruleLabel} source-ip condition has unparseable CIDR(s): ${invalid.join(", ")}. The local ALB front-door requires valid IPv4 / IPv6 CIDRs; skipping the rule.`);
|
|
5398
|
+
return;
|
|
5399
|
+
}
|
|
5400
|
+
out.sourceIpCidrs.push(...values);
|
|
5401
|
+
} else {
|
|
5402
|
+
warnings.push(`${ruleLabel} uses unsupported condition field '${field}'. Skipping the rule.`);
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5072
5405
|
}
|
|
5406
|
+
return out;
|
|
5407
|
+
}
|
|
5408
|
+
/**
|
|
5409
|
+
* Parse an `http-header` condition into its `{ name, values }` form. Returns
|
|
5410
|
+
* `undefined` when `HttpHeaderName` is missing / non-string or `Values` is
|
|
5411
|
+
* empty (either makes the rule's authoring intent unrecoverable).
|
|
5412
|
+
*/
|
|
5413
|
+
function parseHttpHeaderCondition(cond) {
|
|
5414
|
+
const cfg = cond["HttpHeaderConfig"];
|
|
5415
|
+
if (!cfg || typeof cfg !== "object") return void 0;
|
|
5416
|
+
const c = cfg;
|
|
5417
|
+
const name = c["HttpHeaderName"];
|
|
5418
|
+
if (typeof name !== "string" || name.length === 0) return void 0;
|
|
5419
|
+
const values = (Array.isArray(c["Values"]) ? c["Values"] : []).filter((v) => typeof v === "string");
|
|
5420
|
+
if (values.length === 0) return void 0;
|
|
5073
5421
|
return {
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
unsupported
|
|
5422
|
+
name,
|
|
5423
|
+
values
|
|
5077
5424
|
};
|
|
5078
5425
|
}
|
|
5079
5426
|
/**
|
|
5427
|
+
* Parse a `query-string` condition's `Values` into `{ key?, value }` pairs.
|
|
5428
|
+
* ALB accepts either object entries (`{ Key?, Value }`) or bare strings
|
|
5429
|
+
* (legacy v1 shape: a string is treated as `{ value }`). Non-string `Key` /
|
|
5430
|
+
* `Value` fields are dropped; an entry with no `Value` is dropped. Returns
|
|
5431
|
+
* `hadEntries` so the caller can distinguish an absent / empty `Values` array
|
|
5432
|
+
* (drop the field silently) from one whose entries were all unparseable
|
|
5433
|
+
* (warn + skip the whole rule — silently dropping would widen routing).
|
|
5434
|
+
*/
|
|
5435
|
+
function parseQueryStringConditionValues(cond) {
|
|
5436
|
+
const cfg = cond["QueryStringConfig"];
|
|
5437
|
+
const rawValues = cfg && typeof cfg === "object" && Array.isArray(cfg["Values"]) ? cfg["Values"] : Array.isArray(cond["Values"]) ? cond["Values"] : [];
|
|
5438
|
+
const parsed = [];
|
|
5439
|
+
for (const v of rawValues) if (typeof v === "string") parsed.push({ value: v });
|
|
5440
|
+
else if (v && typeof v === "object") {
|
|
5441
|
+
const e = v;
|
|
5442
|
+
const value = e["Value"];
|
|
5443
|
+
if (typeof value !== "string") continue;
|
|
5444
|
+
const key = e["Key"];
|
|
5445
|
+
parsed.push(typeof key === "string" ? {
|
|
5446
|
+
key,
|
|
5447
|
+
value
|
|
5448
|
+
} : { value });
|
|
5449
|
+
}
|
|
5450
|
+
return {
|
|
5451
|
+
parsed,
|
|
5452
|
+
hadEntries: rawValues.length > 0
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5455
|
+
/**
|
|
5456
|
+
* Cheap CIDR validity check used at parse time. Mirrors the more permissive
|
|
5457
|
+
* matcher (`albCidrMatches`) but only confirms shape; we do not need to
|
|
5458
|
+
* remember the parsed bytes here.
|
|
5459
|
+
*/
|
|
5460
|
+
function isValidCidr(value) {
|
|
5461
|
+
const slash = value.indexOf("/");
|
|
5462
|
+
if (slash === -1) return false;
|
|
5463
|
+
const addr = value.slice(0, slash);
|
|
5464
|
+
const prefix = value.slice(slash + 1);
|
|
5465
|
+
if (!/^\d+$/.test(prefix)) return false;
|
|
5466
|
+
const prefixLen = parseInt(prefix, 10);
|
|
5467
|
+
if (addr.includes(".") && !addr.includes(":")) {
|
|
5468
|
+
const parts = addr.split(".");
|
|
5469
|
+
if (parts.length !== 4) return false;
|
|
5470
|
+
if (!parts.every((p) => /^\d+$/.test(p) && parseInt(p, 10) <= 255)) return false;
|
|
5471
|
+
return prefixLen >= 0 && prefixLen <= 32;
|
|
5472
|
+
}
|
|
5473
|
+
if (addr.includes(":")) return prefixLen >= 0 && prefixLen <= 128;
|
|
5474
|
+
return false;
|
|
5475
|
+
}
|
|
5476
|
+
/**
|
|
5080
5477
|
* Extract a condition's string values from either the typed `<Field>Config`
|
|
5081
5478
|
* sub-object's `Values` or the legacy top-level `Values` array.
|
|
5082
5479
|
*/
|
|
@@ -5317,6 +5714,10 @@ function albStrategy(options) {
|
|
|
5317
5714
|
priority: r.priority,
|
|
5318
5715
|
pathPatterns: r.pathPatterns,
|
|
5319
5716
|
hostPatterns: r.hostPatterns,
|
|
5717
|
+
httpHeaderConditions: r.httpHeaderConditions,
|
|
5718
|
+
httpRequestMethods: r.httpRequestMethods,
|
|
5719
|
+
queryStringConditions: r.queryStringConditions,
|
|
5720
|
+
sourceIpCidrs: r.sourceIpCidrs,
|
|
5320
5721
|
action: qualify(r.action)
|
|
5321
5722
|
}))
|
|
5322
5723
|
});
|
|
@@ -5423,4 +5824,4 @@ function createLocalListCommand(opts = {}) {
|
|
|
5423
5824
|
|
|
5424
5825
|
//#endregion
|
|
5425
5826
|
export { createLocalRunTaskCommand as a, createLocalStartServiceCommand as i, formatTargetListing as n, createLocalInvokeAgentCoreCommand as o, createLocalStartAlbCommand as r, createLocalInvokeCommand as s, createLocalListCommand as t };
|
|
5426
|
-
//# sourceMappingURL=local-list-
|
|
5827
|
+
//# sourceMappingURL=local-list-DbBCVhla.js.map
|