22set -euo pipefail
33
44# ===========================================================================
5- # One-time CloudFront setup script for forkzero.ai
6- # Creates/updates: URL rewrite function + security response headers policy
5+ # CloudFront setup script for forkzero.ai
6+ #
7+ # Creates/updates and associates:
8+ # 1. CloudFront Function for URL rewriting + trailing-slash redirects
9+ # 2. Response Headers Policy (security headers)
10+ # 3. Custom error responses (404 → /404.html, 403 → SPA fallback)
711#
812# Prerequisites:
913# - AWS CLI v2 configured with appropriate credentials
1014# - jq installed
1115#
1216# Usage:
13- # chmod +x infra/setup-cloudfront.sh
14- # ./infra/setup-cloudfront.sh
15- #
16- # After running, manually associate the function and headers policy with
17- # your CloudFront distribution's default cache behavior.
17+ # ./infra/setup-cloudfront.sh # auto-detect distribution
18+ # DIST_ID=E1QZI... ./infra/setup-cloudfront.sh # explicit distribution ID
1819# ===========================================================================
1920
2021FUNCTION_NAME=" forkzero-url-rewrite"
2122FUNCTION_FILE=" $( dirname " $0 " ) /cloudfront-url-rewrite.js"
2223HEADERS_POLICY_NAME=" forkzero-security-headers"
24+ DOMAIN=" forkzero.ai"
25+
26+ # ---- Resolve distribution ID ----
27+
28+ if [ -z " ${DIST_ID:- } " ]; then
29+ echo " ==> Looking up CloudFront distribution for ${DOMAIN} ..."
30+ DIST_ID=$( aws cloudfront list-distributions \
31+ --query " DistributionList.Items[?Aliases.Items[?@=='${DOMAIN} ']].Id | [0]" \
32+ --output text)
33+ if [ -z " ${DIST_ID} " ] || [ " ${DIST_ID} " = " None" ]; then
34+ echo " ERROR: No CloudFront distribution found for ${DOMAIN} " >&2
35+ exit 1
36+ fi
37+ fi
38+ echo " Distribution: ${DIST_ID} "
2339
2440# ---- CloudFront Function: URL rewrite ----
41+ echo " "
2542echo " ==> Creating/updating CloudFront Function: ${FUNCTION_NAME} "
2643
27- # Check if the function already exists
28- EXISTING=$( aws cloudfront list-functions --query " FunctionList.Items[?Name=='${FUNCTION_NAME} '].Name" --output text 2> /dev/null || true)
44+ EXISTING=$( aws cloudfront list-functions \
45+ --query " FunctionList.Items[?Name=='${FUNCTION_NAME} '].Name" \
46+ --output text 2> /dev/null || true)
2947
3048if [ -z " ${EXISTING} " ]; then
3149 echo " Creating new function..."
@@ -49,7 +67,8 @@ aws cloudfront publish-function \
4967 --name " ${FUNCTION_NAME} " \
5068 --if-match " ${ETAG} "
5169
52- FUNCTION_ARN=$( aws cloudfront describe-function --name " ${FUNCTION_NAME} " --query ' FunctionSummary.FunctionMetadata.FunctionARN' --output text)
70+ FUNCTION_ARN=$( aws cloudfront describe-function --name " ${FUNCTION_NAME} " \
71+ --query ' FunctionSummary.FunctionMetadata.FunctionARN' --output text)
5372echo " Published: ${FUNCTION_ARN} "
5473
5574# ---- Response Headers Policy: security headers ----
@@ -98,7 +117,6 @@ POLICY_CONFIG=$(cat <<'POLICY_JSON'
98117POLICY_JSON
99118)
100119
101- # Check if the policy already exists
102120EXISTING_POLICY_ID=$( aws cloudfront list-response-headers-policies \
103121 --query " ResponseHeadersPolicyList.Items[?ResponseHeadersPolicy.ResponseHeadersPolicyConfig.Name=='${HEADERS_POLICY_NAME} '].ResponseHeadersPolicy.Id" \
104122 --output text 2> /dev/null || true)
121139
122140echo " Policy ID: ${POLICY_ID} "
123141
124- # ---- Custom Error Response: proper 404 page ----
125- echo " "
126- echo " ==> Configuring custom error response for 404s"
127- echo " "
128- echo " NOTE: CloudFront custom error responses must be set via the distribution"
129- echo " config (not a standalone resource). Run the following to update it:"
130- echo " "
131- echo " DIST_ID=your-distribution-id"
142+ # ---- Associate everything with the distribution ----
132143echo " "
133- echo ' # Get current config'
134- echo ' aws cloudfront get-distribution-config --id $DIST_ID > /tmp/cf-config.json'
135- echo ' ETAG=$(jq -r .ETag /tmp/cf-config.json)'
136- echo " "
137- echo ' # Extract DistributionConfig and add custom error response'
138- echo ' jq ".DistributionConfig.CustomErrorResponses = {'
139- echo ' \"Quantity\": 1,'
140- echo ' \"Items\": [{'
141- echo ' \"ErrorCode\": 404,'
142- echo ' \"ResponsePagePath\": \"/404.html\",'
143- echo ' \"ResponseCode\": \"404\",'
144- echo ' \"ErrorCachingMinTTL\": 300'
145- echo ' }]'
146- echo ' } | .DistributionConfig" /tmp/cf-config.json > /tmp/cf-update.json'
147- echo " "
148- echo ' aws cloudfront update-distribution \'
149- echo ' --id $DIST_ID \'
150- echo ' --distribution-config file:///tmp/cf-update.json \'
151- echo ' --if-match $ETAG'
144+ echo " ==> Updating distribution ${DIST_ID} ..."
145+
146+ # Fetch current config
147+ TMPDIR=$( mktemp -d)
148+ trap ' rm -rf "${TMPDIR}"' EXIT
149+
150+ aws cloudfront get-distribution-config --id " ${DIST_ID} " > " ${TMPDIR} /config.json"
151+ DIST_ETAG=$( jq -r ' .ETag' " ${TMPDIR} /config.json" )
152+
153+ # Build updated config with jq:
154+ # - Set function association on default cache behavior
155+ # - Set response headers policy on default cache behavior
156+ # - Set custom error responses (403 → SPA fallback, 404 → /404.html)
157+ jq --arg fn_arn " ${FUNCTION_ARN} " --arg policy_id " ${POLICY_ID} " '
158+ .DistributionConfig
159+ | .DefaultCacheBehavior.FunctionAssociations = {
160+ "Quantity": 1,
161+ "Items": [{
162+ "FunctionARN": $fn_arn,
163+ "EventType": "viewer-request"
164+ }]
165+ }
166+ | .DefaultCacheBehavior.ResponseHeadersPolicyId = $policy_id
167+ | .CustomErrorResponses = {
168+ "Quantity": 2,
169+ "Items": [
170+ {
171+ "ErrorCode": 403,
172+ "ResponsePagePath": "/404.html",
173+ "ResponseCode": "404",
174+ "ErrorCachingMinTTL": 300
175+ },
176+ {
177+ "ErrorCode": 404,
178+ "ResponsePagePath": "/404.html",
179+ "ResponseCode": "404",
180+ "ErrorCachingMinTTL": 300
181+ }
182+ ]
183+ }
184+ ' " ${TMPDIR} /config.json" > " ${TMPDIR} /update.json"
185+
186+ aws cloudfront update-distribution \
187+ --id " ${DIST_ID} " \
188+ --distribution-config " file://${TMPDIR} /update.json" \
189+ --if-match " ${DIST_ETAG} " \
190+ --query ' Distribution.Status' \
191+ --output text
152192
153- # ---- Instructions ----
154- echo " "
155- echo " ==========================================================================="
156- echo " NEXT STEPS — Associate with your CloudFront distribution:"
157- echo " "
158- echo " 1. Open the CloudFront console → Distributions → your distribution"
159- echo " 2. Edit the Default Cache Behavior:"
160- echo " a. Function associations → Viewer request → CloudFront Functions"
161- echo " → Select '${FUNCTION_NAME} '"
162- echo " b. Response headers policy → Select '${HEADERS_POLICY_NAME} '"
163- echo " 3. Configure Custom Error Response (Error Pages tab):"
164- echo " a. Error code: 404"
165- echo " b. Customize error response: Yes"
166- echo " c. Response page path: /404.html"
167- echo " d. HTTP response code: 404"
168- echo " e. Error caching minimum TTL: 300"
169- echo " 4. Save and wait for deployment to complete"
170193echo " "
171- echo " Or via CLI (replace DIST_ID):"
172- echo " aws cloudfront get-distribution-config --id DIST_ID > dist-config.json"
173- echo " # Edit DefaultCacheBehavior to add:"
174- echo " # FunctionAssociations with ${FUNCTION_ARN} "
175- echo " # ResponseHeadersPolicyId: ${POLICY_ID} "
176- echo " # Edit CustomErrorResponses to add 404 → /404.html mapping"
177- echo " aws cloudfront update-distribution --id DIST_ID --distribution-config file://dist-config.json --if-match ETAG"
178- echo " ==========================================================================="
194+ echo " ==> Done. Distribution is deploying — changes propagate in ~2-5 minutes."
195+ echo " Function: ${FUNCTION_ARN} "
196+ echo " Headers Policy: ${POLICY_ID} "
197+ echo " Error Responses: 403→/404.html (404), 404→/404.html (404)"
0 commit comments