@go-to-k/cdkd 0.119.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 +502 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -457,7 +457,9 @@ cdkd local start-api --from-state # substitute deployed env vars
|
|
|
457
457
|
One server per discovered API — authorizers, CORS configs, and stage
|
|
458
458
|
variables stay scoped to the owning API. Supports REST v1 + HTTP API +
|
|
459
459
|
Function URL with AWS_PROXY integrations; Lambda TOKEN / REQUEST,
|
|
460
|
-
Cognito User Pool,
|
|
460
|
+
Cognito User Pool, HTTP v2 JWT authorizers (JWKS-verified), and REST v1
|
|
461
|
+
`AuthorizationType: 'AWS_IAM'` (SigV4 signature verification only — IAM
|
|
462
|
+
policy evaluation is not emulated; see `docs/local-emulation.md`); CORS
|
|
461
463
|
preflight (HTTP API v2 `CorsConfiguration` + REST v1 OPTIONS MOCK
|
|
462
464
|
preflight from `defaultCorsPreflightOptions`); hot reload via `--watch`;
|
|
463
465
|
deploy-state-backed env var substitution via `--from-state`.
|
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,
|
|
@@ -40468,7 +40468,7 @@ function resolveRestV1Authorizer(authorizerLogicalId, template, stackName, decla
|
|
|
40468
40468
|
declaredAt
|
|
40469
40469
|
};
|
|
40470
40470
|
}
|
|
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
|
|
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).`);
|
|
40472
40472
|
}
|
|
40473
40473
|
/**
|
|
40474
40474
|
* Resolve an `AWS::ApiGatewayV2::Authorizer`. HTTP v2 has only `REQUEST`
|
|
@@ -40777,9 +40777,13 @@ function detectRestV1Authorizer(methodResource, methodLogicalId, stack) {
|
|
|
40777
40777
|
const props = methodResource.Properties ?? {};
|
|
40778
40778
|
const authType = props["AuthorizationType"];
|
|
40779
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
|
+
};
|
|
40780
40785
|
const authorizerId = props["AuthorizerId"];
|
|
40781
40786
|
const refLogicalId = pickRefLogicalId$1(authorizerId);
|
|
40782
|
-
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).`);
|
|
40783
40787
|
if (!refLogicalId) throw new RouteDiscoveryError(`${stack.stackName}/${methodLogicalId}: AuthorizationType='${stringifyValue(authType)}' but AuthorizerId is missing or not a {Ref:...}.`);
|
|
40784
40788
|
return resolveRestV1Authorizer(refLogicalId, stack.template, stack.stackName, `${stack.stackName}/${methodLogicalId}`);
|
|
40785
40789
|
}
|
|
@@ -41454,6 +41458,438 @@ function base64UrlDecodeToBuffer(input) {
|
|
|
41454
41458
|
return Buffer.from(padded + padding, "base64");
|
|
41455
41459
|
}
|
|
41456
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
|
+
|
|
41457
41893
|
//#endregion
|
|
41458
41894
|
//#region src/local/http-server.ts
|
|
41459
41895
|
/**
|
|
@@ -41770,6 +42206,32 @@ async function runAuthorizerPass(authorizer, snapshot, matchCtx, state, opts, re
|
|
|
41770
42206
|
if (cache && result.identityHash !== void 0 && authorizer.resultTtlSeconds > 0) cache.set(authorizer.logicalId, result.identityHash, authorizer.resultTtlSeconds, stripHash(result));
|
|
41771
42207
|
return shapeOutcome(stripHash(result));
|
|
41772
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
|
+
}
|
|
41773
42235
|
if (!opts.jwksCache) return {
|
|
41774
42236
|
result: { allow: false },
|
|
41775
42237
|
denyKind: "policy-deny"
|
|
@@ -41844,6 +42306,10 @@ function buildOverlay(authorizer, result) {
|
|
|
41844
42306
|
kind: "cognito-rest-v1",
|
|
41845
42307
|
claims: result.context ?? {}
|
|
41846
42308
|
};
|
|
42309
|
+
if (authorizer.kind === "iam") return {
|
|
42310
|
+
kind: "lambda-rest-v1",
|
|
42311
|
+
...result.principalId !== void 0 && { principalId: result.principalId }
|
|
42312
|
+
};
|
|
41847
42313
|
return {
|
|
41848
42314
|
kind: "jwt-http-v2",
|
|
41849
42315
|
claims: result.context ?? {}
|
|
@@ -42457,6 +42923,8 @@ async function localStartApiCommand(target, options) {
|
|
|
42457
42923
|
const authorizerCache = createAuthorizerCache();
|
|
42458
42924
|
const jwksCache = createJwksCache();
|
|
42459
42925
|
const jwksWarnedUrls = /* @__PURE__ */ new Set();
|
|
42926
|
+
let sigV4CredentialsLoader;
|
|
42927
|
+
const sigV4WarnedForeignIds = /* @__PURE__ */ new Set();
|
|
42460
42928
|
/**
|
|
42461
42929
|
* One synth + discover + build pass. Returns the next-state
|
|
42462
42930
|
* material. Reused on initial boot AND every hot-reload firing.
|
|
@@ -42575,6 +43043,8 @@ async function localStartApiCommand(target, options) {
|
|
|
42575
43043
|
lastAssetPaths.value = computeAssetPaths(initialMaterial.specs);
|
|
42576
43044
|
await prewarmJwks(initialMaterial.routes, jwksCache);
|
|
42577
43045
|
warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
|
|
43046
|
+
sigV4CredentialsLoader = defaultCredentialsLoader();
|
|
43047
|
+
warnIamRoutes(initialMaterial.routes);
|
|
42578
43048
|
let maxTimeoutSec = 0;
|
|
42579
43049
|
for (const spec of initialMaterial.specs.values()) if (spec.lambda.timeoutSec > maxTimeoutSec) maxTimeoutSec = spec.lambda.timeoutSec;
|
|
42580
43050
|
const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
|
|
@@ -42604,7 +43074,10 @@ async function localStartApiCommand(target, options) {
|
|
|
42604
43074
|
port: basePort === 0 ? 0 : nextPort,
|
|
42605
43075
|
authorizerCache,
|
|
42606
43076
|
jwksCache,
|
|
42607
|
-
jwksWarnedUrls
|
|
43077
|
+
jwksWarnedUrls,
|
|
43078
|
+
sigV4CredentialsLoader,
|
|
43079
|
+
sigV4WarnedForeignIds,
|
|
43080
|
+
sigV4AllowUnverified: options.allowUnverifiedSigv4 === true
|
|
42608
43081
|
});
|
|
42609
43082
|
servers.push({
|
|
42610
43083
|
group,
|
|
@@ -42806,6 +43279,26 @@ function warnVpcConfigLambdas(routesWithAuth, stacks) {
|
|
|
42806
43279
|
}
|
|
42807
43280
|
}
|
|
42808
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
|
+
/**
|
|
42809
43302
|
* Build the per-Lambda container spec — code dir, env vars (template +
|
|
42810
43303
|
* --env-vars overlay), STS-issued creds when --assume-role names this
|
|
42811
43304
|
* Lambda, optional --debug-port reservation. Errors out with a clear
|
|
@@ -43295,7 +43788,7 @@ function parseDebugPort(raw) {
|
|
|
43295
43788
|
* Builder for the `start-api` subcommand. Wired up by `local.ts`.
|
|
43296
43789
|
*/
|
|
43297
43790
|
function createLocalStartApiCommand() {
|
|
43298
|
-
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));
|
|
43299
43792
|
[
|
|
43300
43793
|
...commonOptions,
|
|
43301
43794
|
...appOptions,
|
|
@@ -46224,7 +46717,7 @@ function reorderArgs(argv) {
|
|
|
46224
46717
|
*/
|
|
46225
46718
|
async function main() {
|
|
46226
46719
|
const program = new Command();
|
|
46227
|
-
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");
|
|
46228
46721
|
program.addCommand(createBootstrapCommand());
|
|
46229
46722
|
program.addCommand(createSynthCommand());
|
|
46230
46723
|
program.addCommand(createListCommand());
|