CORS and origins

Worker pickOrigin: local-dev, product hosts, project-scoped Pages previews, ALLOWED_ORIGIN list, Flask constraints.

This page

CORS and origins

Abstract

AXIS is a browser product that talks to cross-origin backends (Flask, Worker, venues). CORS misconfiguration is the #1 local-deploy footgun after missing wheels.

The Worker implements an explicit origin picker (pickOrigin in worker/src/index.ts); Flask and venue APIs have their own rules.

Conceptual model

Diagram

Rendering…

Worker behavior

Resolution order for Access-Control-Allow-Origin:

1. Local-dev (always echoed)

Any http:// or https:// origin on localhost or 127.0.0.1, optional port
(e.g. Vite :3000, axis_pwa_server :8081, Playwright ephemeral ports).

Not allowed (and not needed): http://0.0.0.0:… — browsers do not use 0.0.0.0 as a page origin; binding with HOST=0.0.0.0 only means “listen on all interfaces.”

Localhost/127 allowlisting is safe for CORS: only pages actually served from those hosts present that Origin. A public site cannot forge Origin: http://localhost:3000 in a real browser.

2. Product hosts (2.0.1+)

Exact product / HOOX / legacy PYNE hosts under:

  • *.hoox.sh / hoox.sh
  • *.pynescript.online / pynescript.online

Examples: https://axis.hoox.sh (canonical Pages), https://hoox.sh, https://pynescript.online.

3. Project-scoped Cloudflare® Pages (not open *.pages.dev)

Canonical Pages is https://axis.hoox.sh (product host above). Preview hashes on *.axis.pages.dev (and the apex axis.pages.dev) are also echoed. Legacy *.pynescript-axis.pages.dev still matches. Arbitrary third-party Pages projects (evil.pages.dev, other apps) fall through to the allowlist fallback — they are not reflected.

4. ALLOWED_ORIGIN allowlist

Comma-separated exact origins in env.ALLOWED_ORIGIN (e.g. https://a.example,https://b.example). If the request Origin is listed, echo it; otherwise return the first entry (default https://pynescript.online).

CORS headers applied:

Access-Control-Allow-Origin: <picked>
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Admin-Token, If-Match
Access-Control-Max-Age: 86400
Vary: Origin

OPTIONS204 preflight.

Production implication

For a custom domain already under product regex, no extra var is required. For one-off preview hosts outside the project, list them:

ALLOWED_ORIGIN = "https://my-preview.example.com,https://axis.example.com"

Flask / server engine

The server engine posts from the browser to the configured endpoint. Flask Pro API must:

  1. Answer OPTIONS preflight if cross-origin.
  2. Reflect or allow the PWA origin.
  3. Accept Content-Type: application/json.

Pyne always appends the same product Origin regex as Worker pickOrigin (including *.axis.pages.dev) even when systemd ALLOWED_ORIGINS is a short list. GET /health (AXIS Settings probe) and POST /run are free CORS paths — they echo any Origin so a Pages preview can reach https://axis.hoox.sh without a per-preview allowlist entry.

Same-origin reverse proxy (VPS demo) eliminates CORS when the PWA and Pro API share https://axis.hoox.sh. Cross-origin Pages → VPS still needs those headers (they are now default).

Venue sources/streams

Public Binance/OKX/etc. REST must send CORS headers usable by browsers. Firefox often reports “CORS request did not succeed / status (null)” when the TCP/TLS connection fails entirely (geo block, firewall, extension) — that is not a missing Access-Control-Allow-Origin header.

AXIS resilience for Binance:

  1. Direct hosts: api.binance.com then data-api.binance.vision (src/data/binance-http.ts).
  2. Worker allowlisted proxy: GET /api/market/binance/klines|ticker/24hr|exchangeInfo on the AXIS Worker (worker/src/market.ts).
  3. Live WS rotates :9443:443data-stream.binance.vision (port 9443 is frequently firewalled).
  4. Offline last resort: mock-walk / mock-poll, or warm bars-cache.

Prefer the DO relay (cf-do stream / /api/stream) when many tabs share one upstream WS — still requires the Worker edge to reach Binance.

AXIS resilience for MEXC:

  1. Worker allowlisted proxy first: GET /api/market/mexc/{klines,ticker/24hr,exchangeInfo} (src/data/mexc-http.ts). Public api.mexc.com does not send Access-Control-Allow-Origin.
  2. Direct api.mexc.com only as last-ditch fallback (offline lab, tests, or Worker 5xx). Worker 4xx is not retried against the CORS-blocked host.

Dynamic plugins

  • Module import of plugin JS: needs CORS on the script origin. Prefer same-origin /plugins/* (PYNE Agent ships as /plugins/axis-pine-agent.js).
  • Remote modules (e.g. legacy pyne-agent-worker…/plugin/axis-pine-agent.js) need ACAO + CSP script-src allow when a CSP is present.
  • fetch inside plugin: subject to third-party CORS independently (agent API still uses the pyne-agent Worker origin).

Invariants & edge cases

  1. Credentials — Worker CORS does not set Allow-Credentials: true; use Bearer headers, not cookies.
  2. Multiple prod domains — product regex + comma-separated ALLOWED_ORIGIN cover multi-host; unknown sites never get open reflection.
  3. Vite vs 8081 — both covered for Worker; Flask config must match whichever you use.
  4. Health checks from curl omit Origin — still work; browser calls need correct ACAO.
  5. No open *.pages.dev — only the Pages project axis previews (*.axis.pages.dev) are product-scoped.

Failure modes

Browser consoleMeaning
blocked by CORS policyOrigin not allowlisted
preflight 404Server lacks OPTIONS
No ACAO on error bodySome error paths forgot headers (Worker try/catch mostly covered)

See also