Contents
wrangler.toml
# ============================================================================ # SubX Worker & Pages Configuration — Production Deployment Reference # ============================================================================ # # VERSIONING SCHEMA: # Production: weyland (api.weylandai.com) # Staging: weyland-yutani-staging (workers.dev only) # Dev: weyland-yutani-dev (workers.dev only) # # ============================================================================ # DEPLOYMENT COMMANDS — READ CAREFULLY, FLAGS ARE MANDATORY # ============================================================================ # # +-----------------------------------------------------------------------+ # | WORKER (API backend — weyland-worker.js) | # | | # | Production: npx wrangler deploy | # | Staging: npx wrangler deploy --env=staging | # | Dev: npx wrangler deploy --env=dev | # | | # | Verify: curl https://api.weylandai.com/api/health | # +-----------------------------------------------------------------------+ # # +-----------------------------------------------------------------------+ # | PAGES (Frontend — deployment/public/) | # | | # | *** --branch=production IS MANDATORY — without it, deploys go to | # | *** preview/alpha and production stays STALE. This has caused | # | *** silent failures in sessions 26A01, BOSUN-12, and others. | # | | # | Production: npx wrangler pages deploy deployment/public \ | # | --project-name=submittalexpress \ | # | --branch=production | # | | # | Preview: npx wrangler pages deploy deployment/public \ | # | --project-name=submittalexpress | # | (omitting --branch deploys to preview) | # | | # | Verify: Open https://subx.weylandai.com — check version in footer | # +-----------------------------------------------------------------------+ # # QUICK DEPLOY BOTH (copy-paste): # npx wrangler deploy && npx wrangler pages deploy deployment/public --project-name=submittalexpress --branch=production # # VERSION: 2.4.1 # UPDATED: 2026-02-12 # INDEX: See VERSION_INDEX.md for full stack documentation # FEATURE: Cut Sheet Discovery Engine with Browser Rendering # ============================================================================ name = "weyland" main = "weyland-worker.js" compatibility_date = "2025-10-31" compatibility_flags = ["nodejs_compat"] # Worker settings - Production uses custom domain route workers_dev = true routes = [ { pattern = "api.weylandai.com/*", zone_name = "weylandai.com" }, { pattern = "subx.weylandai.com/quote/*", zone_name = "weylandai.com" }, { pattern = "subx.weylandai.com/q/*", zone_name = "weylandai.com" } ] # Resource limits limits = { cpu_ms = 50000 } # Browser Rendering for Puppeteer-based web scraping [browser] binding = "BROWSER" # D1 Database binding [[d1_databases]] binding = "DB" database_name = "weyland_db" database_id = "882ca221-ec6a-426b-955b-aa57f9ee10aa" # R2 Storage bindings [[r2_buckets]] binding = "UPLOADS" bucket_name = "subx-uploads" [[r2_buckets]] binding = "OUTPUTS" bucket_name = "subx-outputs" # KV Namespaces [[kv_namespaces]] binding = "DEMO_REQUESTS" id = "aa23c8f67dc943448d0cfb247eb797df" [[kv_namespaces]] binding = "CACHE" id = "62ce1167787844fdb3d4f4393f490410" # Queue for Cut Sheet Discovery (producer) [[queues.producers]] queue = "cut-sheet-discovery" binding = "DISCOVERY_QUEUE" # Queue consumer configuration [[queues.consumers]] queue = "cut-sheet-discovery" max_batch_size = 5 max_batch_timeout = 60 max_retries = 3 dead_letter_queue = "cut-sheet-discovery-dlq" # Production environment variables [vars] ENVIRONMENT = "production" VERSION = "2.4.0" LOG_LEVEL = "info" MAX_UPLOAD_SIZE_MB = "50" # 27A: Presigned R2 upload — CF_ACCOUNT_ID is non-secret, R2 keys are secrets CF_ACCOUNT_ID = "f07be5f84583d0d100b05aeeae56870b" ALLOWED_FILE_TYPES = "application/pdf,image/png,image/jpeg" RATE_LIMIT_REQUESTS = "100" RATE_LIMIT_WINDOW_MINUTES = "1" # CORS: Production custom domains (user-facing) + Pages URLs (dev/preview only) # Users NEVER see pages.dev - _worker.js redirects to custom domains CORS_ORIGINS = "https://weylandai.com,https://subx.weylandai.com,https://submittalexpress.pages.dev" # CPS (Catalogue Processing System) API URL - worker calls itself for catalogue searches CPS_API_URL = "https://api.weylandai.com" # Takeoff Express feature flag (WO-2026-0115-TAKEOFF-001) TAKEOFF_ENABLED = "true" # Cron triggers for maintenance tasks [triggers] crons = ["0 2 * * *"] # Observability [observability] enabled = true head_sampling_rate = 1.0 # ============================================================================ # STAGING ENVIRONMENT # Deploy: wrangler deploy --env=staging # URL: weyland-yutani-staging.johnmobley99.workers.dev # ============================================================================ [env.staging] name = "weyland-yutani-staging" workers_dev = true [env.staging.vars] ENVIRONMENT = "staging" VERSION = "2.1.3-staging" LOG_LEVEL = "debug" MAX_UPLOAD_SIZE_MB = "50" ALLOWED_FILE_TYPES = "application/pdf,image/png,image/jpeg" RATE_LIMIT_REQUESTS = "1000" RATE_LIMIT_WINDOW_MINUTES = "1" CORS_ORIGINS = "https://submittalexpress.pages.dev,http://localhost:8787,http://localhost:3000,http://127.0.0.1:8787" [[env.staging.d1_databases]] binding = "DB" database_name = "weyland_db" database_id = "882ca221-ec6a-426b-955b-aa57f9ee10aa" [[env.staging.r2_buckets]] binding = "UPLOADS" bucket_name = "subx-uploads" [[env.staging.r2_buckets]] binding = "OUTPUTS" bucket_name = "subx-outputs" [[env.staging.kv_namespaces]] binding = "DEMO_REQUESTS" id = "aa23c8f67dc943448d0cfb247eb797df" [[env.staging.kv_namespaces]] binding = "CACHE" id = "62ce1167787844fdb3d4f4393f490410" # ============================================================================ # DEV ENVIRONMENT (Local testing against workers.dev) # Deploy: wrangler deploy --env=dev # URL: weyland-yutani-dev.johnmobley99.workers.dev # ============================================================================ [env.dev] name = "weyland-yutani-dev" workers_dev = true [env.dev.vars] ENVIRONMENT = "development" VERSION = "2.1.3-dev" LOG_LEVEL = "debug" MAX_UPLOAD_SIZE_MB = "100" ALLOWED_FILE_TYPES = "application/pdf,image/png,image/jpeg" RATE_LIMIT_REQUESTS = "10000" RATE_LIMIT_WINDOW_MINUTES = "1" CORS_ORIGINS = "*" [[env.dev.d1_databases]] binding = "DB" database_name = "weyland_db" database_id = "882ca221-ec6a-426b-955b-aa57f9ee10aa" [[env.dev.r2_buckets]] binding = "UPLOADS" bucket_name = "subx-uploads" [[env.dev.r2_buckets]] binding = "OUTPUTS" bucket_name = "subx-outputs" [[env.dev.kv_namespaces]] binding = "DEMO_REQUESTS" id = "aa23c8f67dc943448d0cfb247eb797df" [[env.dev.kv_namespaces]] binding = "CACHE" id = "62ce1167787844fdb3d4f4393f490410" # Security headers are handled in worker code (pure JS) # Secrets (set via wrangler secret put) # - ANTHROPIC_API_KEY: Claude API key (sk-ant-...) # - JWT_SECRET: JWT signing secret (generated via openssl rand -base64 32) # - OPENAI_API_KEY: OpenAI API key (if using GPT for fallback) # To set secrets: # wrangler secret put ANTHROPIC_API_KEY # wrangler secret put JWT_SECRET # DEPLOYMENT INSTRUCTIONS FOR ZERO-DEPENDENCY VERSION: # # 1. NO NPM INSTALL REQUIRED! # # 2. Set secrets: # wrangler secret put ANTHROPIC_API_KEY # wrangler secret put JWT_SECRET # # 3. Create D1 database (if not exists): # wrangler d1 create weyland_db # # 4. Run migrations: # wrangler d1 execute weyland_db --file=schema.sql # # 5. Deploy pure JavaScript worker: # wrangler deploy -c wrangler-pure.toml # # 6. Verify deployment: # curl https://weyland.johnmobley99.workers.dev/api/health # # Response should show: # { # "status": "operational", # "dependencies": 0, <-- ZERO dependencies! # "service": "SubX API (Pure JS)" # }
_worker.js
/** * Cloudflare Pages Advanced Mode Worker * Professional SaaS URL Routing for WeylandAI * * URL Architecture: * - weylandai.com → Corporate marketing site * - subx.weylandai.com → SubmittalExpress product app * - *.pages.dev → REDIRECT to custom domain (never shown to users) * * Best Practice: Users should NEVER see deployment platform URLs * Reference: https://developers.cloudflare.com/pages/how-to/redirect-to-custom-domain/ */ export default { async fetch(request, env, ctx) { const url = new URL(request.url); const hostname = url.hostname; // ========================================================================= // RULE 1: Redirect ALL pages.dev URLs to custom domain // Users should NEVER see submittalexpress.pages.dev or any preview URLs // ========================================================================= if (hostname.endsWith('.pages.dev') || hostname.endsWith('.workers.dev')) { // Determine the correct custom domain based on the path let targetHost; // If accessing SubX app paths, redirect to subx.weylandai.com if (url.pathname.startsWith('/subx') || url.pathname.startsWith('/app')) { targetHost = 'subx.weylandai.com'; // Normalize path: /subx.html -> /app, /app -> /app url.pathname = url.pathname === '/subx.html' ? '/app' : url.pathname; } else { // All other paths go to main site targetHost = 'weylandai.com'; } url.hostname = targetHost; url.protocol = 'https:'; // 301 Permanent Redirect - SEO best practice return Response.redirect(url.toString(), 301); } // ========================================================================= // RULE 2: SubX Product Domain (subx.weylandai.com) // ========================================================================= if (hostname === 'subx.weylandai.com') { // Serve .html files directly — bypass Pretty URLs loop if (url.pathname.endsWith('.html')) { return env.ASSETS.fetch(request); } // Serve the SubX application const newUrl = new URL(request.url); // Root path and app aliases serve the app if (url.pathname === '/' || url.pathname === '/app' || url.pathname === '/subx') { newUrl.pathname = '/subx.html'; return env.ASSETS.fetch(newUrl.toString()); } // /login, /signup, /dashboard etc. all serve the SPA if (url.pathname.startsWith('/login') || url.pathname.startsWith('/signup') || url.pathname.startsWith('/dashboard') || url.pathname.startsWith('/projects') || url.pathname.startsWith('/session')) { newUrl.pathname = '/subx.html'; return env.ASSETS.fetch(newUrl.toString()); } // Static assets serve directly return env.ASSETS.fetch(request); } // ========================================================================= // RULE 3: Corporate Domain (weylandai.com) // ========================================================================= if (hostname === 'weylandai.com' || hostname === 'www.weylandai.com') { const newUrl = new URL(request.url); // Root serves marketing homepage if (url.pathname === '/' || url.pathname === '/index.html') { newUrl.pathname = '/index.html'; return env.ASSETS.fetch(newUrl.toString()); } // /products/subx redirects to the product domain if (url.pathname === '/products/subx' || url.pathname === '/subx') { return Response.redirect('https://subx.weylandai.com', 302); } // Serve static assets return env.ASSETS.fetch(request); } // ========================================================================= // FALLBACK: Serve static assets for any other hostname // (This handles local development and edge cases) // ========================================================================= return env.ASSETS.fetch(request); } }
_headers
/* X-Frame-Options: DENY X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: geolocation=(), microphone=(), camera=() Content-Security-Policy: default-src 'self' https://api.weylandai.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://cdn.sheetjs.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://api.weylandai.com; connect-src 'self' https://api.weylandai.com ws://localhost:* http://localhost:*; worker-src blob:; /subx.html Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 /subx Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 /app Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 / Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0
_redirects
# WeylandAI Professional SaaS Redirects # These work with _worker.js to ensure users only see custom domains # SubX Product Routes (subx.weylandai.com) # SPA routes all serve the same HTML, client-side routing handles the rest /app /subx.html 200 /login /subx.html 200 /signup /subx.html 200 /dashboard /subx.html 200 /dashboard/* /subx.html 200 /projects /subx.html 200 /projects/* /subx.html 200 /session/* /subx.html 200 # Marketing Site Routes (weylandai.com) /products/subx https://subx.weylandai.com 302 # Catch-all for SPA (must be last) # Note: API calls should use absolute URLs to api.weylandai.com /* /index.html 200