From abec1ba1a4b72beb025e457b96ce041c50d38de0 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 6 Apr 2026 12:23:34 -0400 Subject: [PATCH 1/5] feat: add SPA route rewrite CloudFront Function to StaticSiteConstruct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attaches a CloudFront Function (viewer request) to the default S3 behavior that rewrites requests with no file extension to /index.html. This enables client-side routing for Blazor WASM and other SPAs without affecting API 404s, which are served via the /api/* behavior (a separate origin) and never reach the default behavior. Previously, missing SPA routes returned a CloudFront-level 404 because the S3 website endpoint serves its error document with a 404 status, and adding a distribution-level 404 → /index.html error response would also swallow legitimate API not-found responses. Co-Authored-By: Claude Sonnet 4.6 --- .../StaticSiteConstruct.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/LayeredCraft.Cdk.Constructs/StaticSiteConstruct.cs b/src/LayeredCraft.Cdk.Constructs/StaticSiteConstruct.cs index 81aa0de..f96f622 100644 --- a/src/LayeredCraft.Cdk.Constructs/StaticSiteConstruct.cs +++ b/src/LayeredCraft.Cdk.Constructs/StaticSiteConstruct.cs @@ -72,6 +72,27 @@ public StaticSiteConstruct(Construct scope, string id, IStaticSiteConstructProps SubjectAlternativeNames = props.AlternateDomains, }); + // CloudFront Function: rewrite SPA routes (no file extension) to /index.html + // so client-side routes are handled by the app rather than returning 404. + // Requests with a file extension pass through unchanged, preserving normal + // asset serving and ensuring API 404s (served via the /api/* behavior) are + // never affected — they never reach this default behavior. + var spaRewriteFunction = new Amazon.CDK.AWS.CloudFront.Function(this, $"{id}-spa-rewrite", + new Amazon.CDK.AWS.CloudFront.FunctionProps + { + Code = FunctionCode.FromInline(""" + function handler(event) { + var uri = event.request.uri; + if (!uri.match(/\.[a-zA-Z0-9]+$/)) { + event.request.uri = '/index.html'; + } + return event.request; + } + """), + Runtime = FunctionRuntime.JS_2_0, + Comment = "Rewrite SPA routes to /index.html for client-side routing" + }); + // Create CloudFront distribution for global content delivery var distribution = new Distribution(this, $"{id}-cdn", new DistributionProps { @@ -83,7 +104,15 @@ public StaticSiteConstruct(Construct scope, string id, IStaticSiteConstructProps ProtocolPolicy = OriginProtocolPolicy.HTTP_ONLY }), AllowedMethods = AllowedMethods.ALLOW_GET_HEAD, - Compress = true + Compress = true, + FunctionAssociations = + [ + new FunctionAssociation + { + Function = spaRewriteFunction, + EventType = FunctionEventType.VIEWER_REQUEST + } + ] }, Certificate = certificate, ErrorResponses = From 5fda07c47e5373b92173a51febb519122dca88f6 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 6 Apr 2026 12:25:22 -0400 Subject: [PATCH 2/5] chore: bump VersionPrefix to 2.4.0 Co-Authored-By: Claude Sonnet 4.6 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index cbf1825..ac5eda0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.3.0 + 2.4.0 MIT From aa9810c6a0563897429c798e78e71a378eb3f546 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 6 Apr 2026 14:01:48 -0400 Subject: [PATCH 3/5] feat(cognito): add PostConfirmationTrigger support to CognitoUserPoolConstruct Callers can now supply an optional IFunction via PostConfirmationTrigger on ICognitoUserPoolConstructProps; when provided it is wired to the UserPool via AddTrigger(UserPoolOperation.POST_CONFIRMATION, ...). Also bumps Amazon.CDK.Lib from 2.246.0 to 2.248.0. Co-Authored-By: Claude Sonnet 4.6 --- Directory.Packages.props | 2 +- src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs | 6 ++++++ .../Models/CognitoUserPoolConstructProps.cs | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0f19abc..3b59b5e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ - + diff --git a/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs b/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs index 65ac0a6..b17ee09 100644 --- a/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs +++ b/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs @@ -2,6 +2,7 @@ using Amazon.CDK; using Amazon.CDK.AWS.CertificateManager; using Amazon.CDK.AWS.Cognito; +using Amazon.CDK.AWS.Lambda; using Amazon.CDK.AWS.Route53; using Amazon.CDK.AWS.Route53.Targets; using Constructs; @@ -63,6 +64,11 @@ public CognitoUserPoolConstruct( RemovalPolicy = props.RemovalPolicy, }); + if (props.PostConfirmationTrigger is not null) + { + UserPool.AddTrigger(UserPoolOperation.POST_CONFIRMATION, props.PostConfirmationTrigger); + } + var resourceServers = CreateResourceServers(props); ResourceServers = resourceServers; diff --git a/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs b/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs index 9f43e13..d79ad9e 100644 --- a/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs +++ b/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs @@ -1,5 +1,6 @@ using Amazon.CDK; using Amazon.CDK.AWS.Cognito; +using Amazon.CDK.AWS.Lambda; namespace LayeredCraft.Cdk.Constructs.Models; @@ -21,6 +22,7 @@ public interface ICognitoUserPoolConstructProps IReadOnlyList AppClients { get; } ICognitoUserPoolDomainProps? Domain { get; } IReadOnlyCollection? Groups { get; } + IFunction? PostConfirmationTrigger { get; } } public sealed record CognitoUserPoolConstructProps : ICognitoUserPoolConstructProps @@ -40,4 +42,5 @@ public sealed record CognitoUserPoolConstructProps : ICognitoUserPoolConstructPr public IReadOnlyList AppClients { get; init; } = []; public ICognitoUserPoolDomainProps? Domain { get; init; } public IReadOnlyCollection? Groups { get; init; } = []; + public IFunction? PostConfirmationTrigger { get; init; } } From 1e1e618f2dc3c8dfdd4a74542ca61d996e74ea2f Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 6 Apr 2026 15:27:16 -0400 Subject: [PATCH 4/5] fix(cognito): grant AdminAddUserToGroup permission to post-confirmation trigger Adds a cognito-idp:AdminAddUserToGroup policy statement to the trigger function's execution role when a PostConfirmationTrigger is provided, resolving the circular dependency caused by referencing the UserPool ARN in the role policy. Co-Authored-By: Claude Sonnet 4.6 --- src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs b/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs index b17ee09..43493fc 100644 --- a/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs +++ b/src/LayeredCraft.Cdk.Constructs/CognitoUserPoolConstruct.cs @@ -2,6 +2,7 @@ using Amazon.CDK; using Amazon.CDK.AWS.CertificateManager; using Amazon.CDK.AWS.Cognito; +using Amazon.CDK.AWS.IAM; using Amazon.CDK.AWS.Lambda; using Amazon.CDK.AWS.Route53; using Amazon.CDK.AWS.Route53.Targets; @@ -67,6 +68,11 @@ public CognitoUserPoolConstruct( if (props.PostConfirmationTrigger is not null) { UserPool.AddTrigger(UserPoolOperation.POST_CONFIRMATION, props.PostConfirmationTrigger); + props.PostConfirmationTrigger.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps + { + Actions = ["cognito-idp:AdminAddUserToGroup"], + Resources = ["*"], + })); } var resourceServers = CreateResourceServers(props); From acaf47cc0564370538af6e6694a9a8481c7f9878 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 6 Apr 2026 15:29:41 -0400 Subject: [PATCH 5/5] chore(cognito): explicit null default for PostConfirmationTrigger Co-Authored-By: Claude Sonnet 4.6 --- .../Models/CognitoUserPoolConstructProps.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs b/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs index d79ad9e..f531168 100644 --- a/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs +++ b/src/LayeredCraft.Cdk.Constructs/Models/CognitoUserPoolConstructProps.cs @@ -42,5 +42,5 @@ public sealed record CognitoUserPoolConstructProps : ICognitoUserPoolConstructPr public IReadOnlyList AppClients { get; init; } = []; public ICognitoUserPoolDomainProps? Domain { get; init; } public IReadOnlyCollection? Groups { get; init; } = []; - public IFunction? PostConfirmationTrigger { get; init; } + public IFunction? PostConfirmationTrigger { get; init; } = null; }