@go-to-k/cdkd 0.118.0 → 0.120.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 -1
- package/dist/cli.js +571 -26
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-CuHRHcyW.js";
|
|
3
3
|
import { $ as RouteDiscoveryError, A as runDockerStreaming, B as AssemblyReader, C as AssetPublisher, D as formatDockerLoginError, E as buildDockerImage, F as resolveCaptureObservedState, H as resolveBucketRegion, I as resolveSkipPrefix, L as resolveStateBucketWithDefault, M as getDefaultStateBucketName, N as getLegacyStateBucketName, O as getDockerCmd, P as resolveApp, Q as ResourceUpdateNotSupportedError, R as resolveStateBucketWithDefaultAndSource, S as shouldRetainResource, T as WorkGraph, W as CdkdError, X as ProvisioningError, Y as PartialFailureError, Z as ResourceTimeoutError, _ as DiffCalculator, _t as withSkipPrefix, a as withRetry, b as LockManager, c as collectInlinePolicyNamesManagedBySiblings, d as normalizeAwsTagsToCfn, dt as runStackBuffered, et as StackHasActiveImportsError, f as resolveExplicitPhysicalId, ft as getLiveRenderer, g as IntrinsicFunctionResolver, gt as generateResourceNameWithFallback, h as assertRegionMatch, ht as generateResourceName, i as withResourceDeadline, j as Synthesizer, k as runDockerForeground, l as CDK_PATH_TAG, lt as getLogger, m as CloudControlProvider, mt as PATTERN_B_RESOURCE_TYPES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, ot as normalizeAwsError, p as ProviderRegistry, pt as PATTERN_B_NAME_PROPERTIES, q as LocalInvokeBuildError, r as DeployEngine, s as IAMRoleProvider, st as withErrorHandling, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as StackTerminationProtectionError, u as matchesCdkPath, v as DagBuilder, vt as withStackName, w as stringifyValue, x as S3StateBackend, y as TemplateParser, z as warnDeprecatedNoPrefixCliFlag } from "./deploy-engine-Chzg_hDE.js";
|
|
4
|
-
import { createHash, createPublicKey, createVerify, randomBytes, randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
6
6
|
import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
|
|
7
7
|
import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ListQueueTagsCommand, ListQueuesCommand, QueueDoesNotExist, SQSClient, SetQueueAttributesCommand, TagQueueCommand, UntagQueueCommand } from "@aws-sdk/client-sqs";
|
|
@@ -39623,7 +39623,7 @@ const MOCK_API_ID = "local";
|
|
|
39623
39623
|
* UTF-8; otherwise base64. Mirrors what API Gateway emits.
|
|
39624
39624
|
*/
|
|
39625
39625
|
function buildHttpApiV2Event(req, ctx, opts = {}) {
|
|
39626
|
-
const { rawPath, rawQueryString } = splitRawUrl(req.rawUrl);
|
|
39626
|
+
const { rawPath, rawQueryString } = splitRawUrl$1(req.rawUrl);
|
|
39627
39627
|
const { headers, cookies } = normalizeHeadersV2(req.headers);
|
|
39628
39628
|
const queryStringParameters = parseQueryStringV2(rawQueryString);
|
|
39629
39629
|
const userAgent = headers["user-agent"] ?? "";
|
|
@@ -39683,7 +39683,7 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
|
|
|
39683
39683
|
* - `pathParameters` may be `null` when there are none (matches AWS).
|
|
39684
39684
|
*/
|
|
39685
39685
|
function buildRestV1Event(req, ctx, opts = {}) {
|
|
39686
|
-
const { rawPath, rawQueryString } = splitRawUrl(req.rawUrl);
|
|
39686
|
+
const { rawPath, rawQueryString } = splitRawUrl$1(req.rawUrl);
|
|
39687
39687
|
const { singular: headers, multi: multiValueHeaders } = normalizeHeadersV1(req.headers);
|
|
39688
39688
|
const { singular: queryStringParameters, multi: multiValueQueryStringParameters } = parseQueryStringV1(rawQueryString);
|
|
39689
39689
|
const contentType = headers["content-type"] ?? "";
|
|
@@ -39772,7 +39772,7 @@ function applyAuthorizerOverlay(event, overlay) {
|
|
|
39772
39772
|
* `rawQueryString` (everything after, or `''`). Neither component is
|
|
39773
39773
|
* decoded — that's the whole point of "raw" per the AWS spec.
|
|
39774
39774
|
*/
|
|
39775
|
-
function splitRawUrl(rawUrl) {
|
|
39775
|
+
function splitRawUrl$1(rawUrl) {
|
|
39776
39776
|
const q = rawUrl.indexOf("?");
|
|
39777
39777
|
if (q === -1) return {
|
|
39778
39778
|
rawPath: rawUrl,
|
|
@@ -40448,18 +40448,27 @@ function resolveRestV1Authorizer(authorizerLogicalId, template, stackName, decla
|
|
|
40448
40448
|
if (type === "COGNITO_USER_POOLS") {
|
|
40449
40449
|
const arns = props["ProviderARNs"];
|
|
40450
40450
|
if (!Array.isArray(arns) || arns.length === 0) throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: COGNITO_USER_POOLS authorizer is missing ProviderARNs.`);
|
|
40451
|
-
const
|
|
40452
|
-
|
|
40451
|
+
const pools = arns.map((entry, idx) => {
|
|
40452
|
+
const arn = pickStringFromArn(entry, `${stackName}/${authorizerLogicalId}.ProviderARNs[${idx}]`);
|
|
40453
|
+
const parsed = parseCognitoUserPoolArn(arn, `${stackName}/${authorizerLogicalId}.ProviderARNs[${idx}]`);
|
|
40454
|
+
return {
|
|
40455
|
+
userPoolArn: arn,
|
|
40456
|
+
region: parsed.region,
|
|
40457
|
+
userPoolId: parsed.userPoolId
|
|
40458
|
+
};
|
|
40459
|
+
});
|
|
40460
|
+
const first = pools[0];
|
|
40453
40461
|
return {
|
|
40454
40462
|
kind: "cognito",
|
|
40455
40463
|
logicalId: authorizerLogicalId,
|
|
40456
|
-
|
|
40457
|
-
|
|
40458
|
-
|
|
40464
|
+
pools,
|
|
40465
|
+
userPoolArn: first.userPoolArn,
|
|
40466
|
+
region: first.region,
|
|
40467
|
+
userPoolId: first.userPoolId,
|
|
40459
40468
|
declaredAt
|
|
40460
40469
|
};
|
|
40461
40470
|
}
|
|
40462
|
-
throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: AWS::ApiGateway::Authorizer.Type '${String(type)}' is not supported by cdkd local start-api (only TOKEN / REQUEST / COGNITO_USER_POOLS
|
|
40471
|
+
throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: AWS::ApiGateway::Authorizer.Type '${String(type)}' is not supported by cdkd local start-api (only TOKEN / REQUEST / COGNITO_USER_POOLS are accepted at the Authorizer resource).`);
|
|
40463
40472
|
}
|
|
40464
40473
|
/**
|
|
40465
40474
|
* Resolve an `AWS::ApiGatewayV2::Authorizer`. HTTP v2 has only `REQUEST`
|
|
@@ -40677,7 +40686,9 @@ function parseCognitoIssuer(issuer) {
|
|
|
40677
40686
|
/**
|
|
40678
40687
|
* Pull a string out of a {Ref} / literal entry under `ProviderARNs`.
|
|
40679
40688
|
* CDK's CognitoUserPoolsAuthorizer emits a literal array of `Fn::GetAtt:
|
|
40680
|
-
* [<UserPool>, 'Arn']` entries — we accept both.
|
|
40689
|
+
* [<UserPool>, 'Arn']` entries — we accept both. The `location` argument
|
|
40690
|
+
* carries the full `<stack>/<authorizer>.ProviderARNs[<idx>]` path so the
|
|
40691
|
+
* error names the offending entry exactly.
|
|
40681
40692
|
*/
|
|
40682
40693
|
function pickStringFromArn(value, location) {
|
|
40683
40694
|
if (typeof value === "string") return value;
|
|
@@ -40685,10 +40696,10 @@ function pickStringFromArn(value, location) {
|
|
|
40685
40696
|
const obj = value;
|
|
40686
40697
|
if ("Fn::GetAtt" in obj) {
|
|
40687
40698
|
const arg = obj["Fn::GetAtt"];
|
|
40688
|
-
if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && arg[1] === "Arn") throw new RouteDiscoveryError(`${location}:
|
|
40699
|
+
if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && arg[1] === "Arn") throw new RouteDiscoveryError(`${location}: uses Fn::GetAtt against logical ID '${arg[0]}'. cdkd local start-api needs the literal ARN string to derive the JWKS URL — set the user pool ARN explicitly via 'authorizer.providerArns' on the CDK construct, or upgrade to JWT (HTTP v2) which encodes the pool in the Issuer URL.`);
|
|
40689
40700
|
}
|
|
40690
40701
|
}
|
|
40691
|
-
throw new RouteDiscoveryError(`${location}:
|
|
40702
|
+
throw new RouteDiscoveryError(`${location}: must be a literal string (got ${shortJson(value)}).`);
|
|
40692
40703
|
}
|
|
40693
40704
|
/**
|
|
40694
40705
|
* Parse and clamp a TTL value to `[0, max]` with a default fallback.
|
|
@@ -40766,9 +40777,13 @@ function detectRestV1Authorizer(methodResource, methodLogicalId, stack) {
|
|
|
40766
40777
|
const props = methodResource.Properties ?? {};
|
|
40767
40778
|
const authType = props["AuthorizationType"];
|
|
40768
40779
|
if (authType === void 0 || authType === "NONE") return void 0;
|
|
40780
|
+
if (authType === "AWS_IAM") return {
|
|
40781
|
+
kind: "iam",
|
|
40782
|
+
logicalId: "AWS_IAM",
|
|
40783
|
+
declaredAt: `${stack.stackName}/${methodLogicalId}`
|
|
40784
|
+
};
|
|
40769
40785
|
const authorizerId = props["AuthorizerId"];
|
|
40770
40786
|
const refLogicalId = pickRefLogicalId$1(authorizerId);
|
|
40771
|
-
if (authType === "AWS_IAM") throw new RouteDiscoveryError(`${stack.stackName}/${methodLogicalId}: REST v1 AWS_IAM authorization is not supported by cdkd local start-api (deferred follow-up PR).`);
|
|
40772
40787
|
if (!refLogicalId) throw new RouteDiscoveryError(`${stack.stackName}/${methodLogicalId}: AuthorizationType='${stringifyValue(authType)}' but AuthorizerId is missing or not a {Ref:...}.`);
|
|
40773
40788
|
return resolveRestV1Authorizer(refLogicalId, stack.template, stack.stackName, `${stack.stackName}/${methodLogicalId}`);
|
|
40774
40789
|
}
|
|
@@ -41206,21 +41221,36 @@ function buildJwksUrlFromIssuer(issuer) {
|
|
|
41206
41221
|
return `${issuer.replace(/\/+$/, "")}/.well-known/jwks.json`;
|
|
41207
41222
|
}
|
|
41208
41223
|
/**
|
|
41209
|
-
*
|
|
41210
|
-
*
|
|
41211
|
-
|
|
41212
|
-
|
|
41224
|
+
* Build the expected `iss` claim URL for a Cognito user pool. Matches the
|
|
41225
|
+
* issuer Cognito embeds in every minted JWT.
|
|
41226
|
+
*/
|
|
41227
|
+
function buildCognitoIssuer(region, userPoolId) {
|
|
41228
|
+
return `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;
|
|
41229
|
+
}
|
|
41230
|
+
/**
|
|
41231
|
+
* Verify a Bearer JWT against the Cognito user pool(s) referenced by the
|
|
41232
|
+
* authorizer. With a multi-pool authorizer (`ProviderARNs[]` of length
|
|
41233
|
+
* 1+) the request-time pool selection is driven by the JWT's unverified
|
|
41234
|
+
* `iss` claim — only the matching pool's JWKS is used for signature
|
|
41235
|
+
* verification, so a token issued by pool A cannot be verified against
|
|
41236
|
+
* pool B's keys. Issuer mismatch against EVERY configured pool rejects
|
|
41237
|
+
* with 401.
|
|
41213
41238
|
*
|
|
41214
41239
|
* Returns `{ allow: false }` on:
|
|
41215
41240
|
* - missing / malformed Authorization header (caller surfaces 401);
|
|
41216
41241
|
* - signature verification failure;
|
|
41217
41242
|
* - expired token (`exp` in the past);
|
|
41218
|
-
* - issuer mismatch (token's `iss` doesn't match
|
|
41243
|
+
* - issuer mismatch (token's `iss` doesn't match any configured pool);
|
|
41219
41244
|
* - audience mismatch (token's `aud` not in the configured allowlist).
|
|
41220
41245
|
*
|
|
41221
41246
|
* Returns `{ allow: true, principalId, context }` on:
|
|
41222
|
-
* - successful verification;
|
|
41223
|
-
* - JWKS-unreachable pass-through mode
|
|
41247
|
+
* - successful verification against the matching pool;
|
|
41248
|
+
* - JWKS-unreachable pass-through mode for the matching pool (with a
|
|
41249
|
+
* warn line on first hit; per-pool TTL handled by the cache).
|
|
41250
|
+
*
|
|
41251
|
+
* Backward compat: a single-element `ProviderARNs[]` (the historical
|
|
41252
|
+
* single-pool case) behaves identically to pre-PR — `pools[0]` is the
|
|
41253
|
+
* only candidate and the `iss` check matches it.
|
|
41224
41254
|
*/
|
|
41225
41255
|
async function verifyCognitoJwt(authorizer, authorizationHeader, jwksCache, opts = {}) {
|
|
41226
41256
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -41230,7 +41260,33 @@ async function verifyCognitoJwt(authorizer, authorizationHeader, jwksCache, opts
|
|
|
41230
41260
|
identityHash: void 0,
|
|
41231
41261
|
ttlSeconds: 0
|
|
41232
41262
|
};
|
|
41233
|
-
|
|
41263
|
+
const pools = authorizer.pools && authorizer.pools.length > 0 ? authorizer.pools : [{
|
|
41264
|
+
userPoolArn: authorizer.userPoolArn,
|
|
41265
|
+
region: authorizer.region,
|
|
41266
|
+
userPoolId: authorizer.userPoolId
|
|
41267
|
+
}];
|
|
41268
|
+
const parsed = parseJwt(token);
|
|
41269
|
+
const identityHash = buildIdentityHash([token]);
|
|
41270
|
+
let selectedPool = pools[0];
|
|
41271
|
+
let issMatched = false;
|
|
41272
|
+
if (parsed && typeof parsed.payload["iss"] === "string") {
|
|
41273
|
+
const tokenIss = parsed.payload["iss"].replace(/\/+$/, "");
|
|
41274
|
+
for (const pool of pools) if (buildCognitoIssuer(pool.region, pool.userPoolId) === tokenIss) {
|
|
41275
|
+
selectedPool = pool;
|
|
41276
|
+
issMatched = true;
|
|
41277
|
+
break;
|
|
41278
|
+
}
|
|
41279
|
+
if (!issMatched) return {
|
|
41280
|
+
allow: false,
|
|
41281
|
+
identityHash,
|
|
41282
|
+
ttlSeconds: 0
|
|
41283
|
+
};
|
|
41284
|
+
} else if (pools.length > 1) return {
|
|
41285
|
+
allow: false,
|
|
41286
|
+
identityHash,
|
|
41287
|
+
ttlSeconds: 0
|
|
41288
|
+
};
|
|
41289
|
+
return verifyAndShape(token, buildCognitoJwksUrl(selectedPool.region, selectedPool.userPoolId), buildCognitoIssuer(selectedPool.region, selectedPool.userPoolId), void 0, jwksCache, opts.warned, now);
|
|
41234
41290
|
}
|
|
41235
41291
|
/**
|
|
41236
41292
|
* Verify a Bearer JWT against an HTTP v2 JWT authorizer's `JwtConfiguration`.
|
|
@@ -41402,6 +41458,438 @@ function base64UrlDecodeToBuffer(input) {
|
|
|
41402
41458
|
return Buffer.from(padded + padding, "base64");
|
|
41403
41459
|
}
|
|
41404
41460
|
|
|
41461
|
+
//#endregion
|
|
41462
|
+
//#region src/local/sigv4-verify.ts
|
|
41463
|
+
/**
|
|
41464
|
+
* SigV4 signature verification for REST v1 `AuthorizationType: 'AWS_IAM'`
|
|
41465
|
+
* authorizers (closes #447).
|
|
41466
|
+
*
|
|
41467
|
+
* # Scope
|
|
41468
|
+
*
|
|
41469
|
+
* cdkd's `cdkd local start-api` runs API Gateway routes locally. When a
|
|
41470
|
+
* route declares `AuthorizationType: 'AWS_IAM'`, AWS-deployed API Gateway
|
|
41471
|
+
* validates the request's SigV4 signature against the calling identity's
|
|
41472
|
+
* IAM permissions. We can't fully reproduce that locally — IAM policy
|
|
41473
|
+
* evaluation requires the deployed IAM data plane — so the local server
|
|
41474
|
+
* does the **signature-verification** half only:
|
|
41475
|
+
*
|
|
41476
|
+
* 1. Parse the `Authorization: AWS4-HMAC-SHA256 ...` header into the
|
|
41477
|
+
* `(credential, signedHeaders, signature)` triple.
|
|
41478
|
+
* 2. Reconstruct the canonical request per
|
|
41479
|
+
* <https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html>.
|
|
41480
|
+
* 3. Derive the signing key from the dev's **local** secret access key
|
|
41481
|
+
* (via the standard AWS SDK credential chain) + the request's date /
|
|
41482
|
+
* region / service scope.
|
|
41483
|
+
* 4. Compare the recomputed signature against the header's `signature`
|
|
41484
|
+
* value (constant-time compare).
|
|
41485
|
+
*
|
|
41486
|
+
* # Local-vs-deployed semantics (per `feedback_match_aws_default_over_opinionated.md`)
|
|
41487
|
+
*
|
|
41488
|
+
* Verification can only succeed when the request was signed with the
|
|
41489
|
+
* **same** credentials the local server can read. When the request's
|
|
41490
|
+
* `Credential=AKID/...` scope names a different access-key-id than the
|
|
41491
|
+
* one the dev has locally, cdkd cannot reproduce the signing key — we
|
|
41492
|
+
* **warn-and-pass** in that case (allow + log a one-line warn), matching
|
|
41493
|
+
* AWS's "verify locally what we can; defer real authorization to deploy
|
|
41494
|
+
* time" model. Refusing would force every dev with a SigV4-signed client
|
|
41495
|
+
* to use the exact same credential the local server sees, which is rarely
|
|
41496
|
+
* what they want.
|
|
41497
|
+
*
|
|
41498
|
+
* Genuinely missing / malformed signatures **are** rejected — those would
|
|
41499
|
+
* never reach the deployed API either.
|
|
41500
|
+
*
|
|
41501
|
+
* # NOT IN SCOPE
|
|
41502
|
+
*
|
|
41503
|
+
* - IAM resource / action / condition policy evaluation. The local server
|
|
41504
|
+
* has no IAM data plane. Signature-verified callers reach the handler
|
|
41505
|
+
* under their own identity; downstream authorization is the dev's
|
|
41506
|
+
* responsibility.
|
|
41507
|
+
* - STS temporary credentials' session-token validation against AWS
|
|
41508
|
+
* (we accept whatever session-token the dev provides locally).
|
|
41509
|
+
* - Multi-account / cross-account signing — we verify against the local
|
|
41510
|
+
* default chain only.
|
|
41511
|
+
*/
|
|
41512
|
+
/**
|
|
41513
|
+
* Default credential loader: instantiates an `STSClient` (a direct cdkd
|
|
41514
|
+
* dependency) and asks its built-in credential provider for the dev's
|
|
41515
|
+
* local credentials. STSClient uses the same Node default credential
|
|
41516
|
+
* chain (env vars → ~/.aws/config → IMDS → ...) every other AWS SDK call
|
|
41517
|
+
* in cdkd uses, so this matches the deploy-time credential resolution
|
|
41518
|
+
* without adding a new dependency.
|
|
41519
|
+
*/
|
|
41520
|
+
function defaultCredentialsLoader() {
|
|
41521
|
+
let cached;
|
|
41522
|
+
return () => {
|
|
41523
|
+
if (cached) return cached;
|
|
41524
|
+
cached = (async () => {
|
|
41525
|
+
const { STSClient } = await import("@aws-sdk/client-sts");
|
|
41526
|
+
const client = new STSClient({});
|
|
41527
|
+
const creds = await client.config.credentials();
|
|
41528
|
+
client.destroy();
|
|
41529
|
+
return {
|
|
41530
|
+
accessKeyId: creds.accessKeyId,
|
|
41531
|
+
secretAccessKey: creds.secretAccessKey,
|
|
41532
|
+
sessionToken: creds.sessionToken
|
|
41533
|
+
};
|
|
41534
|
+
})();
|
|
41535
|
+
return cached;
|
|
41536
|
+
};
|
|
41537
|
+
}
|
|
41538
|
+
/**
|
|
41539
|
+
* Verify the inbound request's `Authorization: AWS4-HMAC-SHA256 ...`
|
|
41540
|
+
* signature against the dev's local credentials.
|
|
41541
|
+
*
|
|
41542
|
+
* Outcomes:
|
|
41543
|
+
* - **No / malformed Authorization header** → `{allow: false}`. The
|
|
41544
|
+
* http-server maps this to 401 (REST v1 `missing-identity`).
|
|
41545
|
+
* - **Signature mismatch** under the dev's own credentials → `{allow: false}`.
|
|
41546
|
+
* The http-server maps this to 403 (REST v1 `policy-deny`).
|
|
41547
|
+
* - **Different `Credential` access-key-id than the dev has** →
|
|
41548
|
+
* `{allow: true}` plus a one-line warn (warn-and-pass; we can't
|
|
41549
|
+
* reproduce a signing key we don't have).
|
|
41550
|
+
* - **Valid signature with the dev's credentials** → `{allow: true}`.
|
|
41551
|
+
* The principal id surfaced to the handler is the parsed
|
|
41552
|
+
* `Credential` access-key-id.
|
|
41553
|
+
*/
|
|
41554
|
+
async function verifySigV4(req, loadCredentials, opts = {}) {
|
|
41555
|
+
const logger = getLogger();
|
|
41556
|
+
const authHeader = pickHeader(req.headers, "authorization");
|
|
41557
|
+
if (!authHeader) return {
|
|
41558
|
+
allow: false,
|
|
41559
|
+
identityHash: void 0
|
|
41560
|
+
};
|
|
41561
|
+
let parsed;
|
|
41562
|
+
try {
|
|
41563
|
+
parsed = parseAuthorizationHeader(authHeader);
|
|
41564
|
+
} catch (err) {
|
|
41565
|
+
logger.debug(`AWS_IAM authorizer: malformed Authorization header — ${err instanceof Error ? err.message : String(err)}`);
|
|
41566
|
+
return {
|
|
41567
|
+
allow: false,
|
|
41568
|
+
identityHash: void 0
|
|
41569
|
+
};
|
|
41570
|
+
}
|
|
41571
|
+
if (parsed.algorithm !== "AWS4-HMAC-SHA256") {
|
|
41572
|
+
logger.debug(`AWS_IAM authorizer: unsupported algorithm '${parsed.algorithm}'`);
|
|
41573
|
+
return {
|
|
41574
|
+
allow: false,
|
|
41575
|
+
identityHash: void 0
|
|
41576
|
+
};
|
|
41577
|
+
}
|
|
41578
|
+
if (parsed.credentialTerminator !== "aws4_request") {
|
|
41579
|
+
logger.debug(`AWS_IAM authorizer: invalid credential scope terminator '${parsed.credentialTerminator}'`);
|
|
41580
|
+
return {
|
|
41581
|
+
allow: false,
|
|
41582
|
+
identityHash: void 0
|
|
41583
|
+
};
|
|
41584
|
+
}
|
|
41585
|
+
const amzDate = pickHeader(req.headers, "x-amz-date") ?? pickHeader(req.headers, "date");
|
|
41586
|
+
if (!amzDate) {
|
|
41587
|
+
logger.debug("AWS_IAM authorizer: missing x-amz-date / date header");
|
|
41588
|
+
return {
|
|
41589
|
+
allow: false,
|
|
41590
|
+
identityHash: void 0
|
|
41591
|
+
};
|
|
41592
|
+
}
|
|
41593
|
+
if (!validateAmzDateMatchesCredentialDate(amzDate, parsed.credentialDate)) {
|
|
41594
|
+
logger.debug(`AWS_IAM authorizer: x-amz-date '${amzDate}' does not match credential scope date '${parsed.credentialDate}'`);
|
|
41595
|
+
return {
|
|
41596
|
+
allow: false,
|
|
41597
|
+
identityHash: void 0
|
|
41598
|
+
};
|
|
41599
|
+
}
|
|
41600
|
+
if (amzDateOutsideSkew(amzDate, (opts.now ?? (() => /* @__PURE__ */ new Date()))())) {
|
|
41601
|
+
logger.debug(`AWS_IAM authorizer: x-amz-date '${amzDate}' outside 15-min clock skew`);
|
|
41602
|
+
return {
|
|
41603
|
+
allow: false,
|
|
41604
|
+
identityHash: void 0
|
|
41605
|
+
};
|
|
41606
|
+
}
|
|
41607
|
+
let local;
|
|
41608
|
+
try {
|
|
41609
|
+
local = await loadCredentials();
|
|
41610
|
+
} catch (err) {
|
|
41611
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
41612
|
+
if (!opts.allowUnverified) {
|
|
41613
|
+
logger.warn(`AWS_IAM authorizer: failed to resolve local AWS credentials (${reason}). Denying request; configure AWS credentials or pass --allow-unverified-sigv4 to opt into the warn-and-pass dev behavior.`);
|
|
41614
|
+
return {
|
|
41615
|
+
allow: false,
|
|
41616
|
+
identityHash: void 0
|
|
41617
|
+
};
|
|
41618
|
+
}
|
|
41619
|
+
logger.warn(`AWS_IAM authorizer: failed to resolve local AWS credentials (${reason}). --allow-unverified-sigv4 is set; passing through with unverified principalId 'unverified-no-creds'. Do NOT trust event.requestContext.identity.accessKey in handler code.`);
|
|
41620
|
+
return {
|
|
41621
|
+
allow: true,
|
|
41622
|
+
principalId: "unverified-no-creds",
|
|
41623
|
+
identityHash: buildIdentityHash([parsed.signature])
|
|
41624
|
+
};
|
|
41625
|
+
}
|
|
41626
|
+
if (local.accessKeyId.toLowerCase() !== parsed.credentialAccessKeyId.toLowerCase()) {
|
|
41627
|
+
const warned = opts.warnedForeignIds;
|
|
41628
|
+
const dedupKey = parsed.credentialAccessKeyId.toLowerCase();
|
|
41629
|
+
if (!opts.allowUnverified) {
|
|
41630
|
+
if (!warned || !warned.has(dedupKey)) {
|
|
41631
|
+
logger.warn(`AWS_IAM authorizer: request signed with foreign access-key-id '${parsed.credentialAccessKeyId}'. Denying; pass --allow-unverified-sigv4 to opt into the warn-and-pass dev behavior, or call with credentials whose access-key-id matches your local one.`);
|
|
41632
|
+
warned?.add(dedupKey);
|
|
41633
|
+
}
|
|
41634
|
+
return {
|
|
41635
|
+
allow: false,
|
|
41636
|
+
identityHash: void 0
|
|
41637
|
+
};
|
|
41638
|
+
}
|
|
41639
|
+
if (!warned || !warned.has(dedupKey)) {
|
|
41640
|
+
logger.warn(`AWS_IAM authorizer: request signed with foreign access-key-id '${parsed.credentialAccessKeyId}'. --allow-unverified-sigv4 is set; passing through with unverified principalId 'unverified-foreign-identity'. Do NOT trust event.requestContext.authorizer.principalId in handler code.`);
|
|
41641
|
+
warned?.add(dedupKey);
|
|
41642
|
+
}
|
|
41643
|
+
return {
|
|
41644
|
+
allow: true,
|
|
41645
|
+
principalId: "unverified-foreign-identity",
|
|
41646
|
+
identityHash: buildIdentityHash([parsed.signature])
|
|
41647
|
+
};
|
|
41648
|
+
}
|
|
41649
|
+
const recomputed = computeSignature(req, parsed, local.secretAccessKey, amzDate);
|
|
41650
|
+
if (!constantTimeEqual(recomputed, parsed.signature)) {
|
|
41651
|
+
logger.debug(`AWS_IAM authorizer: signature mismatch (expected '${recomputed}', got '${parsed.signature}')`);
|
|
41652
|
+
return {
|
|
41653
|
+
allow: false,
|
|
41654
|
+
identityHash: void 0
|
|
41655
|
+
};
|
|
41656
|
+
}
|
|
41657
|
+
return {
|
|
41658
|
+
allow: true,
|
|
41659
|
+
principalId: parsed.credentialAccessKeyId,
|
|
41660
|
+
identityHash: buildIdentityHash([parsed.signature])
|
|
41661
|
+
};
|
|
41662
|
+
}
|
|
41663
|
+
/**
|
|
41664
|
+
* Parse `AWS4-HMAC-SHA256 Credential=..., SignedHeaders=..., Signature=...`.
|
|
41665
|
+
* Rejects every other shape (including legacy `AWS4-HMAC-SHA256-...`
|
|
41666
|
+
* variants and HTTP/1.0-style multi-line values).
|
|
41667
|
+
*/
|
|
41668
|
+
function parseAuthorizationHeader(value) {
|
|
41669
|
+
const spaceIdx = value.indexOf(" ");
|
|
41670
|
+
if (spaceIdx < 0) throw new Error("expected algorithm followed by parameters");
|
|
41671
|
+
const algorithm = value.slice(0, spaceIdx).trim();
|
|
41672
|
+
const parts = value.slice(spaceIdx + 1).trim().split(",").map((s) => s.trim());
|
|
41673
|
+
const fields = {};
|
|
41674
|
+
for (const part of parts) {
|
|
41675
|
+
const eq = part.indexOf("=");
|
|
41676
|
+
if (eq < 0) throw new Error(`malformed parameter '${part}'`);
|
|
41677
|
+
const key = part.slice(0, eq).trim();
|
|
41678
|
+
fields[key] = part.slice(eq + 1).trim();
|
|
41679
|
+
}
|
|
41680
|
+
const credential = fields["Credential"];
|
|
41681
|
+
const signedHeaders = fields["SignedHeaders"];
|
|
41682
|
+
const signature = fields["Signature"];
|
|
41683
|
+
if (!credential) throw new Error("missing Credential");
|
|
41684
|
+
if (!signedHeaders) throw new Error("missing SignedHeaders");
|
|
41685
|
+
if (!signature) throw new Error("missing Signature");
|
|
41686
|
+
const credParts = credential.split("/");
|
|
41687
|
+
if (credParts.length !== 5) throw new Error(`malformed Credential '${credential}' (expected 5 slash-separated segments)`);
|
|
41688
|
+
const [accessKeyId, date, region, service, terminator] = credParts;
|
|
41689
|
+
if (!/^[0-9]{8}$/.test(date)) throw new Error(`malformed credential date '${date}' (expected YYYYMMDD)`);
|
|
41690
|
+
return {
|
|
41691
|
+
algorithm,
|
|
41692
|
+
credentialAccessKeyId: accessKeyId,
|
|
41693
|
+
credentialDate: date,
|
|
41694
|
+
credentialRegion: region,
|
|
41695
|
+
credentialService: service,
|
|
41696
|
+
credentialTerminator: terminator,
|
|
41697
|
+
signedHeaders: signedHeaders.split(";").map((h) => h.trim().toLowerCase()),
|
|
41698
|
+
signature: signature.toLowerCase()
|
|
41699
|
+
};
|
|
41700
|
+
}
|
|
41701
|
+
/**
|
|
41702
|
+
* AWS SigV4 canonical-request computation. Per
|
|
41703
|
+
* <https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html>:
|
|
41704
|
+
*
|
|
41705
|
+
* CanonicalRequest =
|
|
41706
|
+
* HTTPRequestMethod + '\n' +
|
|
41707
|
+
* CanonicalURI + '\n' +
|
|
41708
|
+
* CanonicalQueryString + '\n' +
|
|
41709
|
+
* CanonicalHeaders + '\n' +
|
|
41710
|
+
* SignedHeaders + '\n' +
|
|
41711
|
+
* HexEncode(Hash(RequestPayload))
|
|
41712
|
+
*
|
|
41713
|
+
* Then:
|
|
41714
|
+
* StringToSign = "AWS4-HMAC-SHA256\n" + AmzDate + "\n" +
|
|
41715
|
+
* CredentialScope + "\n" +
|
|
41716
|
+
* HexEncode(Hash(CanonicalRequest))
|
|
41717
|
+
*
|
|
41718
|
+
* SigningKey = HMAC(HMAC(HMAC(HMAC("AWS4"+Secret, Date), Region), Service), "aws4_request")
|
|
41719
|
+
* Signature = HexEncode(HMAC(SigningKey, StringToSign))
|
|
41720
|
+
*/
|
|
41721
|
+
function computeSignature(req, parsed, secretAccessKey, amzDate) {
|
|
41722
|
+
const { path, query } = splitRawUrl(req.rawUrl);
|
|
41723
|
+
const canonicalUri = canonicalizePath(path);
|
|
41724
|
+
const canonicalQuery = canonicalizeQueryString(query);
|
|
41725
|
+
const headerLines = [];
|
|
41726
|
+
for (const name of parsed.signedHeaders) {
|
|
41727
|
+
const raw = pickHeader(req.headers, name);
|
|
41728
|
+
if (raw === void 0) return "missing-signed-header";
|
|
41729
|
+
headerLines.push(`${name}:${normalizeHeaderValue(raw)}\n`);
|
|
41730
|
+
}
|
|
41731
|
+
const canonicalHeaders = headerLines.join("");
|
|
41732
|
+
const signedHeadersStr = parsed.signedHeaders.join(";");
|
|
41733
|
+
const xAmzContentSha = pickHeader(req.headers, "x-amz-content-sha256");
|
|
41734
|
+
const payloadHash = xAmzContentSha && (xAmzContentSha === "UNSIGNED-PAYLOAD" || /^[0-9a-f]{64}$/i.test(xAmzContentSha)) ? xAmzContentSha.toLowerCase() : sha256Hex(req.body);
|
|
41735
|
+
const canonicalRequest = [
|
|
41736
|
+
req.method.toUpperCase(),
|
|
41737
|
+
canonicalUri,
|
|
41738
|
+
canonicalQuery,
|
|
41739
|
+
canonicalHeaders,
|
|
41740
|
+
signedHeadersStr,
|
|
41741
|
+
payloadHash
|
|
41742
|
+
].join("\n");
|
|
41743
|
+
const stringToSign = [
|
|
41744
|
+
"AWS4-HMAC-SHA256",
|
|
41745
|
+
amzDate,
|
|
41746
|
+
`${parsed.credentialDate}/${parsed.credentialRegion}/${parsed.credentialService}/${parsed.credentialTerminator}`,
|
|
41747
|
+
sha256Hex(Buffer.from(canonicalRequest, "utf8"))
|
|
41748
|
+
].join("\n");
|
|
41749
|
+
return hmac(hmac(hmac(hmac(hmac(`AWS4${secretAccessKey}`, parsed.credentialDate), parsed.credentialRegion), parsed.credentialService), "aws4_request"), stringToSign).toString("hex");
|
|
41750
|
+
}
|
|
41751
|
+
function hmac(key, data) {
|
|
41752
|
+
return createHmac("sha256", key).update(data, "utf8").digest();
|
|
41753
|
+
}
|
|
41754
|
+
function sha256Hex(buf) {
|
|
41755
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
41756
|
+
}
|
|
41757
|
+
/**
|
|
41758
|
+
* Split a raw URL into (decoded path, raw query string).
|
|
41759
|
+
*
|
|
41760
|
+
* Important: keep the path RAW for canonicalization — the canonicalizer
|
|
41761
|
+
* does its own URI-encoding so we do NOT decode here.
|
|
41762
|
+
*/
|
|
41763
|
+
function splitRawUrl(rawUrl) {
|
|
41764
|
+
const q = rawUrl.indexOf("?");
|
|
41765
|
+
if (q < 0) return {
|
|
41766
|
+
path: rawUrl,
|
|
41767
|
+
query: ""
|
|
41768
|
+
};
|
|
41769
|
+
return {
|
|
41770
|
+
path: rawUrl.slice(0, q),
|
|
41771
|
+
query: rawUrl.slice(q + 1)
|
|
41772
|
+
};
|
|
41773
|
+
}
|
|
41774
|
+
/**
|
|
41775
|
+
* Canonicalize the request path per the AWS SigV4 spec:
|
|
41776
|
+
*
|
|
41777
|
+
* - URI-encode each path segment (reserved chars are percent-encoded
|
|
41778
|
+
* EXCEPT `-_.~` which stay literal).
|
|
41779
|
+
* - Encode `/` between segments unchanged.
|
|
41780
|
+
* - Empty path → `/`.
|
|
41781
|
+
*
|
|
41782
|
+
* This matches the `execute-api` service's signing rules (no double-
|
|
41783
|
+
* encoding).
|
|
41784
|
+
*/
|
|
41785
|
+
function canonicalizePath(path) {
|
|
41786
|
+
if (!path || path === "") return "/";
|
|
41787
|
+
return path.split("/").map((seg) => {
|
|
41788
|
+
try {
|
|
41789
|
+
return decodeURIComponent(seg);
|
|
41790
|
+
} catch {
|
|
41791
|
+
return seg;
|
|
41792
|
+
}
|
|
41793
|
+
}).join("/").split("/").map((seg) => sigV4EncodePathSegment(seg)).join("/");
|
|
41794
|
+
}
|
|
41795
|
+
/**
|
|
41796
|
+
* Encode a single path segment per the SigV4 unreserved-set rules:
|
|
41797
|
+
* `A-Za-z0-9-_.~` stay literal; everything else is percent-encoded.
|
|
41798
|
+
*/
|
|
41799
|
+
function sigV4EncodePathSegment(seg) {
|
|
41800
|
+
return seg.replace(/[^A-Za-z0-9\-_.~]/g, (ch) => {
|
|
41801
|
+
return encodeURIComponent(ch).replace(/%[0-9a-f]{2}/g, (s) => s.toUpperCase());
|
|
41802
|
+
});
|
|
41803
|
+
}
|
|
41804
|
+
/**
|
|
41805
|
+
* Canonicalize the query string per SigV4: parse `key=value` pairs,
|
|
41806
|
+
* SORT by key (then by value on collisions), URI-encode each side
|
|
41807
|
+
* with upper-case hex, join with `&`.
|
|
41808
|
+
*/
|
|
41809
|
+
function canonicalizeQueryString(query) {
|
|
41810
|
+
if (!query) return "";
|
|
41811
|
+
const pairs = [];
|
|
41812
|
+
for (const raw of query.split("&")) {
|
|
41813
|
+
if (!raw) continue;
|
|
41814
|
+
const eq = raw.indexOf("=");
|
|
41815
|
+
const [k, v] = eq < 0 ? [raw, ""] : [raw.slice(0, eq), raw.slice(eq + 1)];
|
|
41816
|
+
let dk;
|
|
41817
|
+
let dv;
|
|
41818
|
+
try {
|
|
41819
|
+
dk = decodeURIComponent(k.replace(/\+/g, " "));
|
|
41820
|
+
} catch {
|
|
41821
|
+
dk = k;
|
|
41822
|
+
}
|
|
41823
|
+
try {
|
|
41824
|
+
dv = decodeURIComponent(v.replace(/\+/g, " "));
|
|
41825
|
+
} catch {
|
|
41826
|
+
dv = v;
|
|
41827
|
+
}
|
|
41828
|
+
pairs.push([sigV4EncodeQuery(dk), sigV4EncodeQuery(dv)]);
|
|
41829
|
+
}
|
|
41830
|
+
pairs.sort((a, b) => {
|
|
41831
|
+
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
41832
|
+
return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
|
|
41833
|
+
});
|
|
41834
|
+
return pairs.map(([k, v]) => `${k}=${v}`).join("&");
|
|
41835
|
+
}
|
|
41836
|
+
function sigV4EncodeQuery(s) {
|
|
41837
|
+
return s.replace(/[^A-Za-z0-9\-_.~]/g, (ch) => {
|
|
41838
|
+
return encodeURIComponent(ch).replace(/%[0-9a-f]{2}/g, (m) => m.toUpperCase());
|
|
41839
|
+
});
|
|
41840
|
+
}
|
|
41841
|
+
/**
|
|
41842
|
+
* Trim leading/trailing whitespace and collapse internal runs of
|
|
41843
|
+
* whitespace to a single space, per the SigV4 spec.
|
|
41844
|
+
*/
|
|
41845
|
+
function normalizeHeaderValue(value) {
|
|
41846
|
+
return value.trim().replace(/\s+/g, " ");
|
|
41847
|
+
}
|
|
41848
|
+
function pickHeader(headers, name) {
|
|
41849
|
+
const lower = name.toLowerCase();
|
|
41850
|
+
for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === lower) return v;
|
|
41851
|
+
}
|
|
41852
|
+
/**
|
|
41853
|
+
* Compare two hex-encoded signatures in constant time. Returns false
|
|
41854
|
+
* when the lengths differ (the standard short-circuit, since timing
|
|
41855
|
+
* leaks on length are inherent to comparing values of different sizes).
|
|
41856
|
+
*/
|
|
41857
|
+
function constantTimeEqual(a, b) {
|
|
41858
|
+
if (a.length !== b.length) return false;
|
|
41859
|
+
const ab = Buffer.from(a, "hex");
|
|
41860
|
+
const bb = Buffer.from(b, "hex");
|
|
41861
|
+
if (ab.length !== bb.length || ab.length === 0) return false;
|
|
41862
|
+
return timingSafeEqual(ab, bb);
|
|
41863
|
+
}
|
|
41864
|
+
/**
|
|
41865
|
+
* AWS SigV4 expects `x-amz-date` in ISO8601 basic form `YYYYMMDDTHHMMSSZ`.
|
|
41866
|
+
* The credential scope encodes only the date portion. We accept both
|
|
41867
|
+
* `x-amz-date` and the legacy `date` header (RFC 7231) for compat.
|
|
41868
|
+
*/
|
|
41869
|
+
function validateAmzDateMatchesCredentialDate(amzDate, credentialDate) {
|
|
41870
|
+
const isoMatch = /^(\d{8})T\d{6}Z$/.exec(amzDate);
|
|
41871
|
+
if (isoMatch) return isoMatch[1] === credentialDate;
|
|
41872
|
+
try {
|
|
41873
|
+
const parsed = new Date(amzDate);
|
|
41874
|
+
if (Number.isNaN(parsed.getTime())) return false;
|
|
41875
|
+
return `${parsed.getUTCFullYear().toString().padStart(4, "0")}${(parsed.getUTCMonth() + 1).toString().padStart(2, "0")}${parsed.getUTCDate().toString().padStart(2, "0")}` === credentialDate;
|
|
41876
|
+
} catch {
|
|
41877
|
+
return false;
|
|
41878
|
+
}
|
|
41879
|
+
}
|
|
41880
|
+
/**
|
|
41881
|
+
* Reject SigV4 timestamps more than 15 minutes off the local clock —
|
|
41882
|
+
* matches AWS-deployed behavior (the `RequestTimeTooSkewed` error).
|
|
41883
|
+
*/
|
|
41884
|
+
function amzDateOutsideSkew(amzDate, now) {
|
|
41885
|
+
const iso = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(amzDate);
|
|
41886
|
+
let ts;
|
|
41887
|
+
if (iso) ts = new Date(Date.UTC(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]), Number(iso[4]), Number(iso[5]), Number(iso[6])));
|
|
41888
|
+
else ts = new Date(amzDate);
|
|
41889
|
+
if (Number.isNaN(ts.getTime())) return true;
|
|
41890
|
+
return Math.abs(ts.getTime() - now.getTime()) > 900 * 1e3;
|
|
41891
|
+
}
|
|
41892
|
+
|
|
41405
41893
|
//#endregion
|
|
41406
41894
|
//#region src/local/http-server.ts
|
|
41407
41895
|
/**
|
|
@@ -41718,6 +42206,32 @@ async function runAuthorizerPass(authorizer, snapshot, matchCtx, state, opts, re
|
|
|
41718
42206
|
if (cache && result.identityHash !== void 0 && authorizer.resultTtlSeconds > 0) cache.set(authorizer.logicalId, result.identityHash, authorizer.resultTtlSeconds, stripHash(result));
|
|
41719
42207
|
return shapeOutcome(stripHash(result));
|
|
41720
42208
|
}
|
|
42209
|
+
if (authorizer.kind === "iam") {
|
|
42210
|
+
if (!opts.sigV4CredentialsLoader) {
|
|
42211
|
+
getLogger().debug(`AWS_IAM authorizer for ${matchCtx.route.declaredAt}: no SigV4 credentials loader configured — denying.`);
|
|
42212
|
+
return {
|
|
42213
|
+
result: { allow: false },
|
|
42214
|
+
denyKind: "policy-deny"
|
|
42215
|
+
};
|
|
42216
|
+
}
|
|
42217
|
+
const sigResult = await verifySigV4({
|
|
42218
|
+
method: snapshot.method,
|
|
42219
|
+
rawUrl: snapshot.rawUrl,
|
|
42220
|
+
headers,
|
|
42221
|
+
body: snapshot.body
|
|
42222
|
+
}, opts.sigV4CredentialsLoader, {
|
|
42223
|
+
...opts.sigV4WarnedForeignIds && { warnedForeignIds: opts.sigV4WarnedForeignIds },
|
|
42224
|
+
...opts.sigV4AllowUnverified !== void 0 && { allowUnverified: opts.sigV4AllowUnverified }
|
|
42225
|
+
});
|
|
42226
|
+
if (!sigResult.allow) return {
|
|
42227
|
+
result: { allow: false },
|
|
42228
|
+
denyKind: headers["authorization"] !== void 0 ? "policy-deny" : "missing-identity"
|
|
42229
|
+
};
|
|
42230
|
+
return shapeOutcome({
|
|
42231
|
+
allow: true,
|
|
42232
|
+
...sigResult.principalId !== void 0 && { principalId: sigResult.principalId }
|
|
42233
|
+
});
|
|
42234
|
+
}
|
|
41721
42235
|
if (!opts.jwksCache) return {
|
|
41722
42236
|
result: { allow: false },
|
|
41723
42237
|
denyKind: "policy-deny"
|
|
@@ -41792,6 +42306,10 @@ function buildOverlay(authorizer, result) {
|
|
|
41792
42306
|
kind: "cognito-rest-v1",
|
|
41793
42307
|
claims: result.context ?? {}
|
|
41794
42308
|
};
|
|
42309
|
+
if (authorizer.kind === "iam") return {
|
|
42310
|
+
kind: "lambda-rest-v1",
|
|
42311
|
+
...result.principalId !== void 0 && { principalId: result.principalId }
|
|
42312
|
+
};
|
|
41795
42313
|
return {
|
|
41796
42314
|
kind: "jwt-http-v2",
|
|
41797
42315
|
claims: result.context ?? {}
|
|
@@ -42405,6 +42923,8 @@ async function localStartApiCommand(target, options) {
|
|
|
42405
42923
|
const authorizerCache = createAuthorizerCache();
|
|
42406
42924
|
const jwksCache = createJwksCache();
|
|
42407
42925
|
const jwksWarnedUrls = /* @__PURE__ */ new Set();
|
|
42926
|
+
let sigV4CredentialsLoader;
|
|
42927
|
+
const sigV4WarnedForeignIds = /* @__PURE__ */ new Set();
|
|
42408
42928
|
/**
|
|
42409
42929
|
* One synth + discover + build pass. Returns the next-state
|
|
42410
42930
|
* material. Reused on initial boot AND every hot-reload firing.
|
|
@@ -42523,6 +43043,8 @@ async function localStartApiCommand(target, options) {
|
|
|
42523
43043
|
lastAssetPaths.value = computeAssetPaths(initialMaterial.specs);
|
|
42524
43044
|
await prewarmJwks(initialMaterial.routes, jwksCache);
|
|
42525
43045
|
warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
|
|
43046
|
+
sigV4CredentialsLoader = defaultCredentialsLoader();
|
|
43047
|
+
warnIamRoutes(initialMaterial.routes);
|
|
42526
43048
|
let maxTimeoutSec = 0;
|
|
42527
43049
|
for (const spec of initialMaterial.specs.values()) if (spec.lambda.timeoutSec > maxTimeoutSec) maxTimeoutSec = spec.lambda.timeoutSec;
|
|
42528
43050
|
const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
|
|
@@ -42552,7 +43074,10 @@ async function localStartApiCommand(target, options) {
|
|
|
42552
43074
|
port: basePort === 0 ? 0 : nextPort,
|
|
42553
43075
|
authorizerCache,
|
|
42554
43076
|
jwksCache,
|
|
42555
|
-
jwksWarnedUrls
|
|
43077
|
+
jwksWarnedUrls,
|
|
43078
|
+
sigV4CredentialsLoader,
|
|
43079
|
+
sigV4WarnedForeignIds,
|
|
43080
|
+
sigV4AllowUnverified: options.allowUnverifiedSigv4 === true
|
|
42556
43081
|
});
|
|
42557
43082
|
servers.push({
|
|
42558
43083
|
group,
|
|
@@ -42712,7 +43237,7 @@ async function prewarmJwks(routesWithAuth, jwksCache) {
|
|
|
42712
43237
|
for (const entry of routesWithAuth) {
|
|
42713
43238
|
const auth = entry.authorizer;
|
|
42714
43239
|
if (!auth) continue;
|
|
42715
|
-
if (auth.kind === "cognito") urls.add(buildCognitoJwksUrl(
|
|
43240
|
+
if (auth.kind === "cognito") for (const pool of auth.pools) urls.add(buildCognitoJwksUrl(pool.region, pool.userPoolId));
|
|
42716
43241
|
else if (auth.kind === "jwt") {
|
|
42717
43242
|
const url = auth.region && auth.userPoolId ? buildCognitoJwksUrl(auth.region, auth.userPoolId) : buildJwksUrlFromIssuer(auth.issuer);
|
|
42718
43243
|
urls.add(url);
|
|
@@ -42754,6 +43279,26 @@ function warnVpcConfigLambdas(routesWithAuth, stacks) {
|
|
|
42754
43279
|
}
|
|
42755
43280
|
}
|
|
42756
43281
|
/**
|
|
43282
|
+
* Walk the discovered routes for `AuthorizationType: 'AWS_IAM'` and emit
|
|
43283
|
+
* a one-line warn naming the affected routes. Returns `true` when at
|
|
43284
|
+
* least one IAM route is present so the caller wires the SigV4
|
|
43285
|
+
* credentials loader. Re-runs across hot reloads are silent — the warn
|
|
43286
|
+
* fires only at initial boot (matches `warnVpcConfigLambdas`'s policy).
|
|
43287
|
+
*
|
|
43288
|
+
* Implementation note: signature verification only — IAM policy
|
|
43289
|
+
* evaluation (resource / action / condition) is NOT emulated. See
|
|
43290
|
+
* `src/local/sigv4-verify.ts` and the help text in `docs/cli-reference.md`.
|
|
43291
|
+
*/
|
|
43292
|
+
function warnIamRoutes(routesWithAuth) {
|
|
43293
|
+
const logger = getLogger();
|
|
43294
|
+
const iamRoutes = [];
|
|
43295
|
+
for (const entry of routesWithAuth) if (entry.authorizer?.kind === "iam") iamRoutes.push(entry.route.declaredAt);
|
|
43296
|
+
if (iamRoutes.length === 0) return false;
|
|
43297
|
+
logger.warn(`${iamRoutes.length} route(s) declare AuthorizationType: AWS_IAM — cdkd local start-api verifies SigV4 signatures against your local AWS credentials, but does NOT emulate IAM policy evaluation (resource / action / condition rules). Signature-verified callers reach the handler under their own identity; downstream authorization is the dev's responsibility. See docs/cli-reference.md (cdkd local start-api — AWS_IAM authorizer) for details.`);
|
|
43298
|
+
for (const declaredAt of iamRoutes) logger.warn(` - ${declaredAt}`);
|
|
43299
|
+
return true;
|
|
43300
|
+
}
|
|
43301
|
+
/**
|
|
42757
43302
|
* Build the per-Lambda container spec — code dir, env vars (template +
|
|
42758
43303
|
* --env-vars overlay), STS-issued creds when --assume-role names this
|
|
42759
43304
|
* Lambda, optional --debug-port reservation. Errors out with a clear
|
|
@@ -43243,7 +43788,7 @@ function parseDebugPort(raw) {
|
|
|
43243
43788
|
* Builder for the `start-api` subcommand. Wired up by `local.ts`.
|
|
43244
43789
|
*/
|
|
43245
43790
|
function createLocalStartApiCommand() {
|
|
43246
|
-
const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers
|
|
43791
|
+
const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and REST v1 AWS_IAM (SigV4 signature verification only — IAM policy evaluation is NOT emulated; see docs/local-emulation.md). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(localStartApiCommand));
|
|
43247
43792
|
[
|
|
43248
43793
|
...commonOptions,
|
|
43249
43794
|
...appOptions,
|
|
@@ -46172,7 +46717,7 @@ function reorderArgs(argv) {
|
|
|
46172
46717
|
*/
|
|
46173
46718
|
async function main() {
|
|
46174
46719
|
const program = new Command();
|
|
46175
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
46720
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.120.0");
|
|
46176
46721
|
program.addCommand(createBootstrapCommand());
|
|
46177
46722
|
program.addCommand(createSynthCommand());
|
|
46178
46723
|
program.addCommand(createListCommand());
|