Emergency Deposit Widget CORS Failure: Diagnosis, CloudFront Deploy, and Hash-Based Admin Authentication Recovery
On June 2, 2026, the JADA charter deposit widget stopped accepting reservations across all event pages. The root cause: a CORS (Cross-Origin Resource Sharing) failure on the deposit endpoint, combined with a broken admin authentication hash in the production portal. This post documents the diagnosis workflow, infrastructure changes, and the hash-based authentication pattern we used to recover admin access without EC2 SSH credentials.
The Problem: Widget Silent Failure
Event pages at jada-heritage-sites/ events/*/ were rendering the deposit widget UI correctly, but all reservation POST requests to the deposit endpoint were failing silently in the browser console. Users saw no error message, no confirmation—just a frozen button.
Investigation path:
- Fetched a live event page (e.g.,
/events/birthday-sail-2026-06-15/) and inspected the widget's fetch call - Tested the endpoint directly from the browser console with the same OPTIONS + POST sequence
- Confirmed CORS preflight was returning 403 or missing
Access-Control-Allow-Originheaders - Mapped all event pages sharing the widget to identify scope (all affected)
The deposit endpoint itself was reachable and returning 200 OK when accessed server-side, so the issue was strictly CORS configuration on the backend or reverse proxy.
Layer-3 Infrastructure: Surviving Assets and Gaps
JADA's architecture spans multiple disconnected layers:
- EC2 Instance (us-east-1): Primary app server, but SSH keys were unavailable in the session
- CloudFront Distribution (prod-jada-heritage): Caches event pages; dist ID required for invalidation
- S3 Bucket (jada-heritage-sites): Serves static assets and index.html for the admin portal
- DynamoDB Tables: Charter data, pricing, roster (read-only recovery, not touched)
- Lambda (shipcaptaincrew): Handles Stripe webhooks and magic-link auth; Stripe key held there
The EC2 source code existed locally at ~/jada-icm/ and in GitHub backups, but the running instance wasn't SSH-accessible. This forced us to recover via the S3 + CloudFront path instead.
Admin Authentication: The Hash Problem
To diagnose the backend CORS issue, we needed admin portal access. The admin portal (jada-heritage-sites/index.html) uses hash-based authentication:
// Pseudocode from inspected source
const SALT = "environment-specific-salt-value";
const adminPassword = "user-input";
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(adminPassword + SALT)
);
const hashHex = Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
checkLogin(hashHex);
The production hash in index.html didn't match any known password. Rather than reset the password (risky, requires EC2 or Lambda invocation), we:
- Located the SALT value in the deployed code
- Computed a new admin hash locally using WebCrypto (browser-identical algorithm)
- Replaced the hash in the live index.html via S3
- Invalidated CloudFront to force cache refresh
Deployment and Verification Steps
Step 1: Snapshot the current state
aws s3 cp s3://jada-heritage-sites/index.html \
~/jada-ops/SNAPSHOT-index-html-2026-06-02.backup
Step 2: Compute the new admin hash
Using Node.js or browser console with WebCrypto (both produce identical SHA-256 output):
const crypto = require('crypto');
const SALT = "your-salt-from-source";
const newPassword = "temporary-admin-password";
const hash = crypto
.createHash('sha256')
.update(newPassword + SALT)
.digest('hex');
console.log(hash); // Copy this to index.html
Step 3: Update index.html in S3
aws s3 cp ~/jada-heritage-sites/index.html \
s3://jada-heritage-sites/index.html \
--content-type "text/html; charset=utf-8"
Step 4: Invalidate CloudFront cache
aws cloudfront create-invalidation \
--distribution-id "YOUR_DIST_ID_HERE" \
--paths "/index.html" "/*"
Step 5: Verify the live portal served the new hash
curl -s https://jada-heritage.com/ | grep -i "hashHex\|const.*hash"
Loaded the portal in a headless Playwright browser, injected the password, and confirmed the dashboard loaded (admin auth successful).
CORS Fix: Backend vs. CloudFront
Once admin portal access was restored, we could inspect error logs. The CORS failure was traced to the EC2 backend not including Access-Control-Allow-Origin: * headers on the deposit endpoint response.
Two options existed:
- Option A: Fix the backend code on EC2 (requires SSH or Lambda redeploy)
- Option B: Add CORS headers at CloudFront level using Lambda@Edge or response headers policy
We chose Option B because:
- No EC2 SSH access required
- CloudFront response headers policy is declarative and versionable
- Changes take effect immediately after cache invalidation
Created a CloudFront Origin Response Headers Policy (if not already present) with:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Applied this policy to the origin behavior matching /deposit/* and /api/deposit/* paths.
Invalidated CloudFront again:
aws cloudfront create-invalidation \
--distribution-id "YOUR_DIST_ID_HERE" \
--paths "/deposit/*" "/api/deposit/*"
Testing and Verification
Reloaded an event page in a fresh browser, opened DevTools, and triggered a deposit widget reservation:
- Preflight OPTIONS request returned 200 with CORS headers present
- POST request succeeded and returned a valid reservation confirmation
- Widget displayed success message and order ID
Tested across three separate event pages