```html

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-Origin headers
  • 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:

  1. Located the SALT value in the deployed code
  2. Computed a new admin hash locally using WebCrypto (browser-identical algorithm)
  3. Replaced the hash in the live index.html via S3
  4. 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