* fix(slack-oauth): CSP blocked inline script in callback popup
The global vercel.json CSP policy had no 'unsafe-inline' in script-src,
silently blocking the inline <script> in the OAuth callback HTML response.
This prevented window.opener.postMessage() and window.close() from running,
so the popup never closed and the settings UI never refreshed to "Connected".
Fixes:
- Add a targeted CSP override for /api/slack/oauth/callback that permits
'unsafe-inline' scripts (the page only runs postMessage + close, no secrets)
- Broaden the e.origin check in the onMessage listener to accept messages
from any worldmonitor.app origin, not just window.location.origin — the
callback is always served from worldmonitor.app but settings may open on
tech/finance/happy subdomains
- Add two vercel.json CSP tests to prevent regression
* fix(slack-oauth): bind postMessage trust to popup source, not just origin
Any *.worldmonitor.app page could forge a wm:slack_connected message and
trigger saveRuleWithNewChannel. Add e.source === slackOAuthPopup check so
the listener only accepts messages from the exact popup window opened.
* fix(oauth): bypass form-action CSP via JS fetch + fix vercel.json CSP
Root cause: the Connectors WebView treats the page origin as something
other than https://api.worldmonitor.app (likely a null/app origin from
the native shell), so form-action 'self' blocked the consent form POST.
Changes:
- Consent form now uses JavaScript fetch() as primary submission path.
form-action CSP only restricts HTML form submissions; fetch() is
governed by connect-src, which already allows https:. The HTML form
action remains as fallback with the absolute URL.
- Server detects X-Requested-With: fetch header and returns JSON
{ location } on success (so JS can navigate the WebView) or
{ error, nonce } on invalid key (so JS can update the form without
a page reload). Native form POST path still returns 302.
- vercel.json: add sha256 hash of new inline script to script-src,
and add explicit https://api.worldmonitor.app to form-action as
belt-and-suspenders for browsers that resolve 'self' unexpectedly.
* feat(oauth): redesign consent/error pages with WorldMonitor UI + fix CSP script hash
- Redesign htmlError() and consentPage() to match WorldMonitor dashboard
aesthetic (dark #0a0a0a bg, ui-monospace font, #2d8a6e teal, sharp corners)
- Add globe SVG logo, MCP capabilities list, and /pro link to consent page
- Fix vercel.json script-src hash: was sha256-1wYO... (computed from raw escape
sequence) now sha256-GNMh... (computed from evaluated Unicode ellipsis)
- Script logic and DOM IDs unchanged; hash is now byte-perfect with served HTML
* fix(oauth): remove X-Requested-With header + allow null origin for WebView
Fixes two real P1 bugs caught in review:
1. X-Requested-With header made the fetch a non-simple CORS request, which
triggered a preflight. The OPTIONS handler returned no
Access-Control-Allow-Headers and vercel.json /oauth/* only allowed
Content-Type/Authorization — so the preflight failed, and the form
submission never reached the server.
Fix: replace custom header detection with _js=1 POST body field.
Content-Type: application/x-www-form-urlencoded is a simple CORS type
with no custom headers, so no preflight is triggered.
2. WebView with opaque origin sends Origin: null (the literal string).
The old check blocked any origin != https://api.worldmonitor.app,
which returned 403 for every WebView submission.
Fix: allow 'null' explicitly. The CSRF nonce (server-stored, atomic
GETDEL, 10-min TTL) provides the actual security — origin is a
defense-in-depth layer, not the primary guard.
Also adds 4 structural tests in edge-functions.test.mjs that will catch
regressions on both issues.
* feat(mcp): full OAuth 2.1 compliance — Authorization Code + PKCE + DCR
Adds the complete OAuth 2.1 flow required by claude.ai (and any MCP
2025-03-26 client) on top of the existing client_credentials path.
New endpoints:
- POST /oauth/register — Dynamic Client Registration (RFC 7591)
Strict allowlist: claude.ai/claude.com callbacks + localhost only.
90-day sliding TTL on client records (no unbounded Redis growth).
- GET/POST /oauth/authorize — Consent page + authorization code issuance
CSRF nonce binding, X-Frame-Options: DENY, HTML-escapes all metadata,
shows exact redirect hostname, Origin validation (must be our domain).
Stores full SHA-256 of API key (not fingerprint) in auth code.
- authorization_code + refresh_token grants in /oauth/token
Both use GETDEL for atomic single-use consumption (no race condition).
Refresh tokens carry family_id for future family invalidation.
Returns 401 invalid_client when DCR client is expired (triggers re-reg).
Security improvements:
- verifyPkceS256() validates code_verifier format before any SHA-256 work;
returns null=invalid_request vs false=invalid_grant.
- Full SHA-256 (64 hex) stored for new OAuth tokens; legacy
client_credentials keeps 16-char fingerprint (backward compat).
- Discovery doc: only authorization_code + refresh_token advertised.
- Protected resource metadata: /.well-known/oauth-protected-resource
- WWW-Authenticate headers include resource_metadata param.
- HEAD /mcp returns 200 (Anthropic probe compatibility).
- Origin validation on POST /mcp: claude.ai/claude.com + absent allowed.
- ping method + tool annotations (readOnlyHint, openWorldHint).
- api/oauth/ subdir added to edge-function module isolation scan.
* fix(oauth): distinguish Redis unavailable from key-miss; fix retry nonce; extend client TTL on token use
P1: redisGetDel and redisGet in token.js and authorize.js now throw on
transport/HTTP errors instead of swallowing them as null. Callers catch
and return 503 so clients know to retry, not discard valid codes/tokens.
Key-miss (result=null from Redis) still returns null as before.
P2a: Invalid API key retry path now generates and stores a fresh nonce
before re-rendering the consent form. Previously the new nonce was never
persisted, causing the next submit to fail with "Session Expired" and
forcing the user to restart the entire OAuth flow on a single typo.
P2b: token.js now extends the client TTL (EXPIRE, fire-and-forget) after
a successful client lookup in both authorization_code and refresh_token
paths. CLIENT_TTL_SECONDS was defined but unused — clients that only
refresh tokens would expire after 90 days despite continuous use.
* fix(oauth): atomic nonce consumption via GETDEL; fail closed on nonce storage failure
P2a: Nonce storage result is now checked before rendering the consent page
(both initial GET and invalid-key retry path). If redisSet returns false
(storage unavailable), we return a 503-style error page instead of
rendering a form the user cannot submit successfully.
P2b: CSRF nonce is now consumed atomically via GETDEL instead of a
read-then-fire-and-forget-delete. Two concurrent POST submits can no
longer both pass validation before the delete lands, and the delete is
no longer vulnerable to edge runtime isolate teardown.
Adds a /mcp rewrite in vercel.json pointing to /api/mcp, so MCP clients
(Claude Desktop, etc.) can connect to https://api.worldmonitor.app/mcp
instead of /api/mcp.
Benefits: cleaner URL for docs/config, CF bot-blocking rules target /api/*
only so /mcp bypasses them without needing extra CF rule exceptions.
Also adds CORS headers for /mcp and excludes it from the SPA rewrite and
no-cache catch-all patterns. Updates deploy-config test to match new pattern.
* fix(widgets): restore iframe content after drag, remove color-cycle button
- Fix drag-induced blank content: use WeakMap keyed by iframe element to persist
HTML across DOM moves; persistent load listener (no {once}) re-posts on every
browser re-navigation triggered by drag/drop repositioning
- Remove cycleAccentColor, ACCENT_COLORS, and colorBtn from CustomWidgetPanel
header; chatBtn (sparkle) and PRO badge remain; applyAccentColor kept for
saved specs
- Update tests: remove ACCENT_COLORS count test, saveWidget persistence test,
and changeAccent i18n assertion (all for deleted feature)
* fix(widgets): use correct postMessage key 'html' not 'storedHtml'
* fix(widgets): remove duplicate panel title header, fix sandbox CSP beacon error
- System prompt: NEVER add .panel-header or title to widget body; outer panel
frame already shows the title; updated both basic and PRO prompts
- widget-sanitizer: strip leading .panel-header from generated HTML as safety
net in both wrapWidgetHtml and wrapProWidgetHtml
- vercel.json: add https://static.cloudflareinsights.com to sandbox script-src
so Cloudflare beacon injection no longer triggers CSP console errors
* fix(widgets): correct iframe font by anchoring html+body font-family with !important
* fix(widget): allow cdn.jsdelivr.net in sandbox CSP + Sentry error tracking
- Fix Chart.js source map noise: relax sandbox connect-src from 'none' to
https://cdn.jsdelivr.net (both vercel.json header and meta CSP in buildWidgetDoc)
- Add Sentry API 5xx capture in premiumFetch via reportServerError() -- fires on
any status >= 500 before response is returned, tags kind: api_5xx
- Add securitypolicyviolation listener in main.ts for parent-page CSP violations,
filters browser-extension and blob origins, tags kind: csp_violation
* feat(widget): inject panel design system into PRO widget sandbox
Problem: PRO widgets used a disconnected design (large bold titles,
custom tab buttons, hardcoded hex colors) because the sandbox iframe
had no panel CSS classes and the agent had no examples to follow.
Fix:
- buildWidgetDoc: add .panel-header, .panel-title, .panel-tabs,
.panel-tab, .panel-tab.active, .disp-stats-grid, .disp-stat-box,
.disp-stat-value, .disp-stat-label, and --accent CSS variable to
the iframe's <style> block so they work without a custom <style>
- WIDGET_PRO_SYSTEM_PROMPT: add concrete HTML examples for panel
header+tabs, stat boxes, and Chart.js color setup using CSS vars;
prohibit h1/h2/h3 large titles; document the switchTab() pattern
- Test: assert all panel classes and --accent are present in document
Agent now has classes to USE instead of inventing its own styling.
* feat(widget-agent): open API allowlist to all /api/ paths with compact taxonomy
Problem: widget agent only knew 14 hardcoded endpoints and prioritized
search_web even when a WorldMonitor data source was available.
- Replace WIDGET_ALLOWED_ENDPOINTS Set with isWidgetEndpointAllowed()
function: permits any /api/ path, blocks inference/write endpoints
(analyze-stock, backtest-stock, summarize-article, classify-event, etc.)
- Replace per-URL endpoint lists in both WIDGET_SYSTEM_PROMPT and
WIDGET_PRO_SYSTEM_PROMPT with a compact service-grouped taxonomy:
service + method names only, no full URL repeated 60 times (~400
tokens vs ~1200 for 4x more endpoint coverage)
- Strengthen prioritization: "ALWAYS use first, ONLY fall back to
search_web if no matching service exists" (was "preferred for topics")
- Add 30+ new endpoints: earthquakes, wildfires, cyber threats, sanctions,
consumer prices, FRED series, BLS, Big Mac, fuel, grocery, ETF flows,
shipping rates, chokepoints, critical minerals, GPS interference, etc.
* fix(csp): add safari-web-extension: scheme to CSP violation filter
* fix(widgets): fix CSP violations in pro widget iframe by using sandbox page
srcdoc iframes inherit the parent page's Content-Security-Policy response
headers. The parent's hash-based script-src blocks inline scripts and
cdn.jsdelivr.net (Chart.js), making pro widgets silently broken.
Fix: replace srcdoc with a dedicated /wm-widget-sandbox.html page that
has its own permissive CSP via vercel.json route headers. Widget HTML is
passed via postMessage after the sandbox page loads.
- Add public/wm-widget-sandbox.html: minimal relay page that receives
HTML via postMessage and renders it with document.open/write/close.
Validates message origin against known worldmonitor.app domains.
- vercel.json: add CSP override route for sandbox page (unsafe-inline +
cdn.jsdelivr.net), exclude from SPA rewrite and no-cache rules.
- widget-sanitizer.ts: switch wrapProWidgetHtml to src + data-wm-id,
store widget bodies in module-level Map, auto-mount via MutationObserver.
Fix race condition (always use load event, not readyState check).
Delete store entries after mount to prevent memory leak.
- tests: update 4 tests to reflect new postMessage architecture.
* test(deploy): update deploy-config test for wm-widget-sandbox.html exclusion
vercel.json header injection for /api/(.*) was overriding the
Access-Control-Allow-Headers returned by individual edge functions,
so api/widget-agent OPTIONS preflights never included X-Widget-Key
regardless of what the function itself returned.
* feat(auth): integrate better-auth with @better-auth/infra dash plugin
Wire up better-auth server config with the dash() plugin from
@better-auth/infra, and the matching sentinelClient() on the
client side. Adds BETTER_AUTH_API_KEY to .env.example.
* feat(auth): swap @better-auth/infra for @convex-dev/better-auth
[10-01 task 1] Install @convex-dev/better-auth@0.11.2, remove
@better-auth/infra, delete old server/auth.ts skeleton, rewrite
auth-client.ts to use crossDomainClient + convexClient plugins.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): create Convex auth component files
[10-01 task 2] Add convex.config.ts (register betterAuth component),
auth.config.ts (JWT/JWKS provider), auth.ts (better-auth server with
Convex adapter, crossDomain + convex plugins), http.ts (mount auth
routes with CORS). Uses better-auth/minimal for lighter bundle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add admin, organization, and dash plugins
[10-01] Re-install @better-auth/infra for dash() plugin to enable
dash.better-auth.com admin dashboard. Add admin() and organization()
plugins from better-auth/plugins for user and org management.
Update both server (convex/auth.ts) and client (auth-client.ts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): drop @better-auth/infra (Node.js deps incompatible with Convex V8)
Keep admin() and organization() from better-auth/plugins (V8-safe).
@better-auth/infra's dash() transitively imports SAML/SSO with
node:crypto, fs, zlib — can't run in Convex's serverless runtime.
Dashboard features available via admin plugin endpoints instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(11-01): create auth-state.ts with OTT handler and session subscription
- Add initAuthState() for OAuth one-time token verification on page load
- Add subscribeAuthState() reactive wrapper around useSession nanostore atom
- Add getAuthState() synchronous snapshot getter
- Export AuthUser and AuthSession types for UI consumption
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(11-01): add Google OAuth provider and wire initAuthState into App.ts
- Add socialProviders.google with GOOGLE_CLIENT_ID/SECRET to convex/auth.ts
- Add all variant subdomains to trustedOrigins for cross-subdomain CORS
- Call initAuthState() in App.init() before panelLayout.init()
- Add authModal field to AppContext interface (prepares for Plan 02)
- Add authModal: null to App constructor state initialization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(11-02): create AuthModal with Sign In/Sign Up tabs and Google OAuth
- Sign In tab: email/password form calling authClient.signIn.email()
- Sign Up tab: name/email/password form calling authClient.signUp.email()
- Google OAuth button calling authClient.signIn.social({ provider: 'google', callbackURL: '/' })
- Auto-close on successful auth via subscribeAuthState() subscription
- Escape key, overlay click, and X button close the modal
- Loading states, error display, and client-side validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(11-02): add AuthHeaderWidget, mount in header, add auth CSS
- AuthHeaderWidget: reactive header widget showing Sign In button (anonymous) or avatar + dropdown (authenticated)
- User dropdown: name, email, Free tier badge, Sign Out button calling authClient.signOut()
- setupAuthWidget() in EventHandlerManager creates modal + widget, mounts at authWidgetMount span
- authWidgetMount added to panel-layout.ts header-right, positioned before download wrapper
- setupAuthWidget() called from App.ts after setupUnifiedSettings()
- Full auth CSS: modal styles, tabs, forms, Google button, header widget, avatar, dropdown
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(11-02): add localhost:3000 to trustedOrigins for local dev CORS
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): remove admin/organization plugins that break Convex adapter validator
The admin() plugin adds banned/role fields to user creation data, but the
@convex-dev/better-auth adapter validator doesn't include them. These plugins
are Phase 12 work — will re-add with additionalFields config when needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(12-01): add Resend email transport, verification + reset callbacks, role field
- Install resend SDK for transactional email
- Add emailVerification with sendOnSignUp:true and fire-and-forget Resend callbacks
- Add sendResetPassword callback with 1-hour token expiry
- Add user.additionalFields.role (free/pro, input:false, defaultValue:free)
- Create userRoles fallback table in schema with by_userId index
- Create getUserRole query and setUserRole mutation in convex/userRoles.ts
- Lazy-init Resend client to avoid Convex module analysis error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(12-01): enhance auth-state with emailVerified and role fields
- Add emailVerified (boolean) and role ('free' | 'pro') to AuthUser interface
- Fetch role from Convex userRoles table via HTTP query after session hydration
- Cache role per userId to avoid redundant fetches
- Re-notify subscribers asynchronously when role is fetched for a new user
- Map emailVerified from core better-auth user field (default false)
- Derive Convex cloud URL from VITE_CONVEX_SITE_URL env var
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore(12-01): add Convex generated files from deployment
- Track convex/_generated/ files produced by npx convex dev --once
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(12-03): create panel-gating service with auth-aware showGatedCta
- Add PanelGateReason enum (NONE/ANONYMOUS/UNVERIFIED/FREE_TIER)
- Add getPanelGateReason() computing gating from AuthSession + premium flag
- Add Panel.showGatedCta() rendering auth-aware CTA overlays
- Add Panel.unlockPanel() to reverse locked state
- Extract lockSvg to module-level const shared by showLocked/showGatedCta
- Add i18n keys: signInToUnlock, signIn, verifyEmailToUnlock, resendVerification, upgradeDesc, upgradeToPro
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(12-02): add forgot password flow, password reset form, and token detection
- Widen authModal interface in app-context.ts to support reset-password mode and setResetToken
- AuthModal refactored with 4 views: signin, signup, forgot-password, reset-password
- Forgot password view sends reset email via authClient.requestPasswordReset
- Reset password form validates matching passwords and calls authClient.resetPassword
- auth-state.ts detects ?token= param from email links, stores as pendingResetToken
- App.ts routes pending reset token to auth modal after UI initialization
- CSS for forgot-link, back-link, and success message elements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(12-02): add email verification banner to AuthHeaderWidget and tier badge
- Show non-blocking verification banner below header for unverified users
- Banner has "Resend" button calling authClient.sendVerificationEmail
- Banner is dismissible (stored in sessionStorage, reappears next session)
- Tier badge dynamically shows Free/Pro based on user.role
- Pro badge has gradient styling distinct from Free badge
- Dropdown shows unverified status indicator with yellow dot
- Banner uses fixed positioning, does not push content down
- CSS for banner, pro badge, and verification status indicators
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(12-03): wire reactive auth-based gating into panel-layout
- Add WEB_PREMIUM_PANELS Set (stock-analysis, stock-backtest, daily-market-brief)
- Subscribe to auth state changes in PanelLayoutManager.init()
- Add updatePanelGating() iterating panels with getPanelGateReason()
- Add getGateAction() returning CTA callbacks per gate reason
- Remove inline showLocked() calls for web premium panels
- Preserve desktop _lockPanels for forecast, oref-sirens, telegram-intel
- Clean up auth subscription in destroy()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(13-01): create auth-token utility and inject Bearer header in web fetch redirect
- Add src/services/auth-token.ts with getSessionBearerToken() that reads session token from localStorage
- Add WEB_PREMIUM_API_PATHS Set for the 4 premium market API paths
- Inject Authorization: Bearer header in installWebApiRedirect() for premium paths when session exists
- Desktop installRuntimeFetchPatch() left unchanged (API key only)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(13-01): create server-side session validation module
- Add server/auth-session.ts with validateBearerToken() for Vercel edge gateway
- Validates tokens via Convex /api/auth/get-session with Better-Auth-Cookie header
- Falls back to userRoles:getUserRole Convex query for role resolution
- In-memory cache with 60s TTL and 100-entry cap
- Network errors not cached to allow retry on next request
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(13-02): add bearer token fallback auth for premium API endpoints
- Dynamic import of auth-session.ts when premium endpoint + API key fails
- Valid pro session tokens fall through to route handler
- Non-pro authenticated users get 403 'Pro subscription required'
- Invalid/expired tokens get 401 'Invalid or expired session'
- Non-premium endpoints and static API key flow unchanged
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): sign-in button invisible in dark theme — white on white
--accent is #fff in dark theme, so background: var(--accent) + color: #fff
was invisible. Changed to transparent background with var(--text) color.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): add premium panel keys to full and finance variant configs
stock-analysis, stock-backtest, and daily-market-brief were defined in
the shared panels.ts but missing from variant DEFAULT_PANELS, causing
shouldCreatePanel() to return false and panel gating CTAs to never render.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(auth): add Playwright smoke tests for auth UI (phases 12-13)
6 tests covering: Sign In button visibility, auth modal opening,
modal views (Sign In/Sign Up/Forgot Password), premium panel gating
for anonymous users, and auth token absence when logged out.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): remove role additionalField that breaks Convex component validator
The betterAuth Convex component has a strict input validator for the
user model that doesn't include custom fields. The role additionalField
caused ArgumentValidationError on sign-up. Roles are already stored in
the separate userRoles table — no data loss.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): use Authorization Bearer header for Convex session validation
Better-Auth-Cookie header returned null — the crossDomain plugin's
get-session endpoint expects Authorization: Bearer format instead.
Confirmed via curl against live Convex deployment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): use verified worldmonitor.app domain for auth emails
Was using noreply@resend.dev (testing domain) which can't send to
external recipients. Switched to noreply@worldmonitor.app matching
existing waitlist/contact emails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): await Resend email sends — Convex kills dangling promises
void (fire-and-forget) causes Convex to terminate the fetch before
Resend receives it. Await ensures emails actually get sent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: update Convex generated auth files after config changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): guard against undefined VITE_CONVEX_SITE_URL in auth-state
The Convex cloud URL derivation crashed the entire app when
VITE_CONVEX_SITE_URL wasn't set in the build environment (Vercel
preview). Now gracefully defaults to empty string and skips role
fetching when the URL is unavailable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add dash + organization plugins, remove Google OAuth, fix dark mode button
- Add @better-auth/infra dash plugin for hosted admin dashboard
- Add organization plugin for org management in dashboard
- Add dash.better-auth.com to trustedOrigins
- Remove Google OAuth (socialProviders, button, divider, CSS)
- Fix auth submit button invisible in dark mode (var(--accent) → #3b82f6)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): replace dash plugin with admin — @better-auth/infra incompatible with Convex V8
@better-auth/infra imports SSO/SAML libraries requiring Node.js built-ins
(crypto, fs, stream) which Convex's V8 runtime doesn't support.
Replaced with admin plugin from better-auth/plugins which provides
user management endpoints (set-role, list-users, ban, etc.) natively.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove stale Convex generated files after plugin update
Convex dev regenerated _generated/ — the per-module JS files
(auth.js, http.js, schema.js, etc.) are no longer emitted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore(auth): remove organization plugin — will add in subsequent PR
Organization support (team accounts, invitations, member management)
is not wired into any frontend flow yet. Removing to keep the auth
PR focused on email/password + admin endpoints. Will add back when
building the org/team feature.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add authentication & panel gating guide
Documents the auth stack, panel gating configuration, server-side
session enforcement, environment variables, and user roles.
Includes step-by-step guide for adding new premium panels.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(test): stub panel-gating in RuntimeConfigPanel test harness
Panel.ts now imports @/services/panel-gating, which wasn't stubbed —
causing the real runtime.ts (with window.location) to be bundled,
breaking Node.js tests with "ReferenceError: location is not defined".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): allow Vercel preview origins in Convex trustedOrigins
* fix(auth): broaden Convex trustedOrigins to cover *.worldmonitor.app previews
* fix(auth): use hostonly wildcard pattern for *.worldmonitor.app in trustedOrigins
* fix(auth): add Convex site origins to trustedOrigins
* fix(ci): add convex/ to vercel-ignore watched paths
* fix(auth): remove admin() plugin — adds banned/role fields rejected by Convex validator
* fix(auth): remove admin() plugin — injects banned/role fields rejected by Convex betterAuth validator
* feat(auth): replace email/password with email OTP passwordless flow
- Replace emailAndPassword + emailVerification with emailOTP plugin
- Rewrite AuthModal: email entry -> OTP code verification (no passwords)
- Remove admin() plugin (caused Convex schema validation errors)
- Remove email verification banner and UNVERIFIED gate reason (OTP
inherently verifies email)
- Remove password reset flow (forgot/reset password views, token handling)
- Clean up unused CSS (tabs, verification banner, success messages)
- Update docs to reflect new passwordless auth stack
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(quick-2): harden Convex userRoles and add role cache TTL
- P0: Convert setUserRole from mutation to internalMutation (not callable from client)
- P2: Add 5-minute TTL to role cache in auth-state.ts
- P2: Add localStorage shape warning on auth-token.ts
- P3: Document getUserRole public query trade-off
- P3: Fix misleading cache comment in auth-session.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(quick-2): auth widget teardown, E2E test rewrite, gateway comment
- P2: Store authHeaderWidget on AppContext, destroy in EventHandlerManager.destroy()
- P2: Also destroy authModal in destroy() to prevent leaked subscriptions
- P1: Rewrite E2E tests for 2-view OTP modal (email input + submit button)
- P1: Remove stale "Sign Up" and "Forgot Password" test assertions
- P2: Replace flaky waitForTimeout(5000) with Playwright auto-retry assertion
- P3: Add clarifying comment on premium bearer-token fallback in gateway
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(header): restructure header/footer, add profile editing, pro-gate playback/export
- Remove version, @eliehabib, GitHub link, and download button from header
- Move version + @eliehabib credit to footer brand line; download link to footer nav
- Move auth widget (profile avatar) to far right of header (after settings gear)
- Add default generic SVG avatar for users with no image and no name
- Add profile editing in auth dropdown: display name + avatar URL with Save/Cancel
- Add Settings shortcut in auth dropdown (opens UnifiedSettings)
- Gate Historical Playback and Export controls behind pro role (hidden for free users)
- Reactive pro-gate: subscribes to auth state changes, stores unsub in proGateUnsubscribers[]
- Clean up proGateUnsubscribers on EventHandlerManager.destroy() to prevent leaks
- Fix: render Settings button unconditionally (hidden via style), stable DOM structure
- Fix: typed updateUser call with runtime existence check instead of (any) cast
- Make initFooterDownload() private to match class conventions
* feat(analytics): add Umami auth integration and event tracking
- Wire analytics.ts facade to Umami (port from main #1914):
search, country, map layers, panels, LLM, theme, language,
variant switch, webcam, download, findings, deeplinks
- Add Window.umami shim to vite-env.d.ts
- Add initAuthAnalytics() that subscribes to auth state and calls
identifyUser(id, role) / clearIdentity() on sign-in/sign-out
- Add trackSignIn, trackSignUp, trackSignOut, trackGateHit exports
- Call initAuthAnalytics() from App.ts after initAuthState()
- Track sign-in/sign-up (via isNewUser flag) in AuthModal OTP verify
- Track sign-out in AuthHeaderWidget before authClient.signOut()
- Track gate-hit for export, playback (event-handlers) and pro-banner
* feat(auth): professional avatar widget with colored initials and clean profile edit
- Replace white-circle avatar with deterministic colored initials (Gmail/Linear style)
- Avatar color derived from email hash across 8-color palette
- Dropdown redesigned: row layout with large avatar + name/email/tier info
- Profile edit form: name-only (removed avatar URL field)
- Remove Settings button from dropdown (gear icon in header is sufficient)
- Discord community widget: single CTA link, no redundant text label
- Add all missing CSS for dropdown interior, profile edit form, menu items
* fix(auth): lock down billing tier visibility and fix TOCTOU race
P1: getUserRole converted to internalQuery — billing tier no longer
accessible via any public Convex client API. Exposed only through
the new authenticated /api/user-role HTTP action which validates
the session Bearer token before returning the role.
P1: subscribeAuthState generation counter + AbortController prevents
rapid sign-in/sign-out from delivering stale role for wrong user.
P2: typed RawSessionUser/RawSessionValue interfaces replace any casts
at the better-auth nanostore boundary. fetchUserRole drops userId
param — server derives identity from Bearer token only.
P2: isNewUser heuristic removed from OTP verify — better-auth emailOTP
has no reliable isNewUser signal. All verifications tracked as
trackSignIn. OTP resend gets 30s client-side cooldown.
P2: auth-token.ts version pin comment added (better-auth@1.5.5 +
@convex-dev/better-auth@0.11.2). Gateway inner PREMIUM_RPC_PATHS
comment clarified to explain why it is not redundant.
Adds tests/auth-session.test.mts: 11 tests covering role fallback
endpoint selection, fail-closed behavior, and CORS origin matching.
* feat(quick-4): replace better-auth with Clerk JS -- packages, Convex config, browser auth layer
- Remove better-auth, @convex-dev/better-auth, @better-auth/infra, resend from dependencies
- Add @clerk/clerk-js and jose to dependencies
- Rewrite convex/auth.config.ts for Clerk issuer domain
- Simplify convex/convex.config.ts (remove betterAuth component)
- Delete convex/auth.ts, convex/http.ts, convex/userRoles.ts
- Remove userRoles table from convex/schema.ts
- Create src/services/clerk.ts with Clerk JS init, sign-in, sign-out, token, user metadata, UserButton
- Rewrite src/services/auth-state.ts backed by Clerk (same AuthUser/AuthSession interface)
- Delete src/services/auth-client.ts (better-auth client)
- Delete src/services/auth-token.ts (localStorage token scraping)
- Update .env.example with Clerk env vars, remove BETTER_AUTH_API_KEY
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(quick-4): UI components, runtime fetch, server-side JWT, CSP, and tests
- Delete AuthModal.ts, create AuthLauncher.ts (thin Clerk.openSignIn wrapper)
- Rewrite AuthHeaderWidget.ts to use Clerk UserButton + openSignIn
- Update event-handlers.ts to use AuthLauncher instead of AuthModal
- Rewrite runtime.ts enrichInitForPremium to use async getClerkToken()
- Rewrite server/auth-session.ts for jose-based JWT verification with cached JWKS
- Update vercel.json CSP: add *.clerk.accounts.dev to script-src and frame-src
- Add Clerk CSP tests to deploy-config.test.mjs
- Rewrite e2e/auth-ui.spec.ts for Clerk UI
- Rewrite auth-session.test.mts for jose-based validation
- Use dynamic import for @clerk/clerk-js to avoid Node.js test breakage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): allow Clerk Pro users to load premium data on web
The data-loader gated premium panel loading (stock-analysis, stock-backtest,
daily-market-brief) on WORLDMONITOR_API_KEY only, which is desktop-only.
Web users with Clerk Pro auth were seeing unlocked panels stuck on "Loading..."
because the requests were never made.
Added hasPremiumAccess() helper that checks for EITHER desktop API key OR
Clerk Pro role, matching the migration plan Phase 7 requirements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): address PR #1812 review — all 4 merge blockers + 3 gaps
Blockers:
1. Remove stale Convex artifacts (http.js, userRoles.js, betterAuth
component) from convex/_generated/api.d.ts
2. isProUser() now checks getAuthState().user?.role === 'pro' alongside
legacy localStorage keys
3. Finance premium refresh scheduling now fires for Clerk Pro web users
(not just API key holders)
4. JWT verification now validates audience: 'convex' to reject tokens
scoped to other Clerk templates
Gaps:
5. auth-session tests: 10 new cases (valid pro/free, expired, wrong
key/audience/issuer, missing sub/plan, JWKS reuse) using self-signed
keys + local JWKS server
6. premium-stock-gateway tests: 4 new bearer token cases (pro→200,
free→403, invalid→401, public unaffected)
7. docs/authentication.mdx rewritten for Clerk (removed all better-auth
references, updated stack/files/env vars/roles sections)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address P1 reactive Pro UI + P2 daily-market-brief + P3 stale env vars
P1 — In-session Pro UI changes no longer require a full reload:
- setupExportPanel: removed early isProUser() return, always creates
and relies on reactive subscribeAuthState show/hide
- setupPlaybackControl: same pattern — always creates, reactive gate
- Custom widget panels: always loaded regardless of Pro status
- Pro add-panel and MCP add-panel blocks: always rendered, shown/hidden
reactively via subscribeAuthState callback
- Flight search wiring: always wired, checks Pro status inside callback
so mid-session sign-ins work immediately
P2 — daily-market-brief added to hasPremiumAccess() block in loadAllData()
so Clerk Pro web users get initial data load (was only primed in
primeVisiblePanelData, missing from the general reload path)
P3 — Removed stale CONVEX_SITE_URL and VITE_CONVEX_SITE_URL from
docs/authentication.mdx env vars table (neither is referenced in codebase)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add isProUser import, populate PREMIUM_RPC_PATHS, and fix bearer token auth flow
- Added missing isProUser import in App.ts (fixes typecheck)
- Populated PREMIUM_RPC_PATHS with stock analysis endpoints
- Restructured gateway auth: trusted browser origins bypass API key for
premium endpoints (client-side isProUser gate), while bearer token
validation runs as a separate step for premium paths when present
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(gateway): require credentials for premium paths + defer free-tier enforcement until auth ready
P0: Removed trusted-origin bypass for premium endpoints — Origin header
is spoofable and cannot be a security boundary. Premium paths now always
require either an API key or valid bearer token.
P1: Deferred panel/source free-tier enforcement until auth state resolves.
Previously ran in the constructor before initAuthState(), causing Clerk Pro
users to have their panels/sources trimmed on every startup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): apply WorldMonitor design system to Clerk modal
Theme-aware appearance config passed to clerk.load(), openSignIn(),
and mountUserButton(). Dark mode: dark bg (#111), green primary
(#44ff88), monospace font. Light mode: white bg, green-600 primary
(#16a34a). Reads document.documentElement.dataset.theme at call time
so theme switches are respected.
* fix(auth): gate Clerk init and auth widget behind BETA_MODE
Clerk auth initialization and the Sign In header widget are now only
activated when localStorage `worldmonitor-beta-mode` is set to "true",
allowing silent deployment for internal testing before public rollout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(auth): gate Clerk init and auth widget behind isProUser()
Clerk auth initialization and the Sign In header widget are now only
activated when the user has wm-widget-key or wm-pro-key in localStorage
(i.e. isProUser() returns true), allowing silent deployment for internal
testing before public rollout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(data-loader): replace stale isProUser() with hasPremiumAccess()
loadMarketImplications() still referenced the removed isProUser import,
causing a TS2304 build error. Align with the rest of data-loader.ts
which uses hasPremiumAccess() (checks both API key and Clerk auth).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(auth): address PR #1812 review — P1 security fixes + P2 improvements
P1 fixes:
- Add algorithms: ['RS256'] allowlist to jwtVerify (prevents alg:none bypass)
- Reset loadPromise on Clerk init failure (allows retry instead of permanent breakage)
P2 fixes:
- Extract PREMIUM_RPC_PATHS to shared module (eliminates server/client divergence risk)
- Add fail-fast guard in convex/auth.config.ts for missing CLERK_JWT_ISSUER_DOMAIN
- Add 50s token cache with in-flight dedup to getClerkToken() (prevents concurrent races)
- Sync Clerk CSP entries to index.html and tauri.conf.json (previously only in vercel.json)
- Type clerkInstance as Clerk instead of any
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(auth): clear cached token on signOut()
Prevents stale token from being returned during the ≤50s cache window
after a user signs out.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Sebastien Melki <sebastien@anghami.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Sebastien Melki <sebastienmelki@gmail.com>
* feat(analytics): add Umami analytics via self-hosted instance
Adds Umami analytics script from abacus.worldmonitor.app and updates
CSP headers in both index.html and vercel.json to allow the script.
* feat(analytics): complete Umami integration with event tracking
- Add data-domains to index.html script to exclude dev traffic
- Add Umami script to /pro page and blog (Base.astro)
- Add TypeScript Window.umami shim to vite-env.d.ts
- Wire analytics.ts facade to Umami (replaces PostHog no-ops):
search, country clicks, map layers, panels, LLM usage, theme,
language, variant switch, webcam, download, findings, deeplinks
- Add direct callsite tracking for: settings-open, mcp-connect-attempt,
mcp-connect-success, mcp-panel-add, widget-ai-open/generate/success,
news-summarize, news-sort-toggle, live-news-fullscreen,
webcam-fullscreen, search-open (desktop/mobile/fab)
* fix(analytics): add Tauri CSP allowlist for Umami + skip programmatic layer events
- Add abacus.worldmonitor.app to Tauri CSP script-src and connect-src
so Umami loads in the desktop WebView (analytics exception to the
no-cloud-data rule — needed to know if desktop is used)
- Filter trackMapLayerToggle to user-initiated events only to avoid
inflating counts with programmatic toggles on page load
Vercel preview toolbar and preview URLs were blocked by missing CSP entries.
Both the meta tag (index.html) and HTTP header (vercel.json) must be in sync
since browsers enforce all active policies simultaneously.
- Add sha256-903UI9... hash to index.html script-src (was in vercel.json only,
causing production inline script blocks from the meta tag policy)
- Add https://vercel.live and https://*.vercel.app to frame-src in both files
- Add https://vercel.live and https://*.vercel.app to frame-ancestors in vercel.json
/docs was proxied to Mintlify's /docs endpoint which returns 404.
Mintlify's content lives at /docs/documentation. Replace the broken
proxy rewrite with a 302 redirect so /docs lands correctly.
* fix(docs): exclude /docs from CSP header that blocks Mintlify scripts
The catch-all /(.*) header rule applied Content-Security-Policy with
SHA-based script-src to all routes including /docs/*. Mintlify generates
dozens of inline scripts that don't match those hashes, causing 71 CSP
errors and a completely blank docs page.
Fix: change catch-all to /((?!docs).*) so /docs paths inherit only
their own lightweight headers (nosniff, HSTS, referrer-policy).
* fix(tests): update deploy-config test for docs CSP exclusion
Test was looking for exact source '/(.*)', updated to match the new
'/((?!docs).*)' pattern that excludes /docs from the strict CSP.
* fix: unblock geolocation and fix stale CSP hash for SW nuke script
Permissions-Policy had geolocation=() which blocked navigator.geolocation
used by user-location.ts. Changed to geolocation=(self).
CSP script-src had a stale SHA-256 hash (903UI9my...) that didn't match the
current SW nuke script content. The script was silently blocked in production,
preventing recovery from stale service workers after deploys. Replaced with
the correct hash (4Z2xtr1B...) in both vercel.json and index.html meta tag.
* test: update permissions-policy test for geolocation=(self)
Move geolocation from "disabled" list to "delegated" assertions since
it now allows self-origin access for user-location.ts.
* fix(data): restore bootstrap and cache test coverage
* test: cover runtime env guardrails
* fix(test): align security header tests with current vercel.json
Update catch-all source pattern, geolocation policy value, and
picture-in-picture origins to match current production config.
* perf: reduce uncached API calls via client-side circuit breaker caches
Add client-side circuit breaker caches with IndexedDB persistence to the
top 3 uncached API endpoints (CF analytics: 10.5M uncached requests/day):
- classify-events (5.37M/day): 6hr cache per normalized title, shouldCache
guards against caching null/transient failures
- get-population-exposure (3.45M/day): 6hr cache per coordinate key
(toFixed(4) for ~11m precision), 64-entry LRU
- summarize-article (1.68M/day): 2hr cache per headline-set hash via
buildSummaryCacheKey, eliminates both cache-check and summarize RPCs
Fix workbox-*.js getting no-cache headers (3.62M/day): exclude from SPA
catch-all regex in vercel.json, add explicit immutable cache rule for
content-hashed workbox files.
Migrate USNI fleet fetch from Vercel edge to Railway relay (gold standard):
- Add seedUSNIFleet() loop to ais-relay.cjs (6hr interval, gzip support)
- Make server handler Redis-read-only (435 lines reduced to 38)
- Move usniFleet from ON_DEMAND to BOOTSTRAP_KEYS in health.js
- Add persistCache + shouldCache to client breaker
Estimated reduction: ~14.3M uncached requests/day.
* fix: address code review findings (P1 + P2)
P1: Include SummarizeOptions in summary cache key to prevent cross-option
cache pollution (e.g. cloud summary replayed after user disables cloud LLMs).
P2: Document that forceRefresh is intentionally ignored now that USNI
fetching moved to Railway relay (Vercel is Redis-read-only).
* fix: reject forceRefresh explicitly instead of silently ignoring it
Return an error response with explanation when forceRefresh=true is sent,
rather than silently returning cached data. Makes the behavior regression
visible to any caller instead of masking it.
* fix(build): set worker.format to 'es' for Vite 6 compatibility
Vite 6 defaults worker.format to 'iife' which fails with code-splitting
workers (analysis.worker.ts uses dynamic imports). Setting 'es' fixes
the Vercel production build.
* fix(test): update deploy-config test for workbox regex exclusion
The SPA catch-all regex test hard-coded the old pattern without the
workbox exclusion. Update to match the new vercel.json source pattern.
* fix(csp): add commodity variant to CSP and fix iframe variant navigation
- Add commodity.worldmonitor.app to frame-src and frame-ancestors in
vercel.json and index.html CSP — was missing while all other variants
were listed
- Open variant links in new tab when app runs inside an iframe to prevent
sandbox navigation errors ("This content is blocked")
- Add allow-popups and allow-popups-to-escape-sandbox to pro page iframe
sandbox attribute
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(csp): add missing variant subdomains to tauri.conf.json frame-src
Sync tauri.conf.json CSP with index.html and vercel.json by adding
finance, commodity, and happy worldmonitor.app subdomains to frame-src.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add PR screenshots for CSP fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Set up Mintlify to serve docs at worldmonitor.app/docs via Vercel rewrites
proxying to worldmonitor.mintlify.dev.
- Add mint.json with navigation (5 doc groups + 22 OpenAPI API references)
- Add Vercel rewrites for /docs, exclude from SPA catch-all and no-cache rules
- Exclude /docs from CSP headers (Mintlify manages its own scripts/styles)
- Add frontmatter to all 18 navigation docs for proper Mintlify rendering
- Fix internal links from ./FILE.md to /FILE format for Mintlify routing
- Convert ../path links to GitHub URLs (source code references)
- Add .mintlifyignore for internal docs (Docs_To_Review, roadmap, etc.)
- Copy app icon as logo.png and favicon.png
* feat(blog): add Astro blog at /blog with 16 SEO-optimized posts
Adds a static Astro blog built during Vercel deploy and served at
worldmonitor.app/blog. Includes 16 marketing/SEO posts covering
features, use cases, and comparisons from customer perspectives.
- blog-site/: Astro static site with content collections, RSS, sitemap
- Vercel build pipeline: build:blog builds Astro and copies to public/blog/
- vercel.json: exclude /blog from SPA catch-all rewrite and no-cache headers
- vercel.json: ignoreCommand triggers deploy on blog-site/ changes
- Cache: /blog/_astro/* immutable, blog HTML uses Vercel defaults
* fix(blog): fix markdown lint errors in blog posts
Add blank lines around headings (MD022) and lists (MD032) across
all 16 blog post files to pass markdownlint checks.
* fix(ci): move ignoreCommand to script to stay under 256 char limit
Vercel schema validates ignoreCommand max length at 256 characters.
Move the logic to scripts/vercel-ignore.sh and reference it inline.
* fix(blog): address PR review findings
- Add blog sitemap to robots.txt for SEO discovery
- Use www.worldmonitor.app consistently (canonical domain)
- Clean public/blog/ before copy to prevent stale files
- Use npm ci for hermetic CI builds
* fix(blog): move blog dependency install to postinstall phase
Separates dependency installation from compilation. Blog deps are
now installed during npm install (postinstall hook), not during build.
* fix(csp): allow cross-subdomain framing and add finance to frame-src
frame-ancestors 'self' blocked tech/finance variants from rendering
inside the Pro landing page iframe. Widen to *.worldmonitor.app.
Also adds missing finance.worldmonitor.app to frame-src.
Closes#1322
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(csp): remove conflicting X-Frame-Options and tighten frame-ancestors
X-Frame-Options: SAMEORIGIN contradicts the new frame-ancestors directive
that allows cross-subdomain framing. Modern browsers prioritize
frame-ancestors over X-Frame-Options, but sending both is contradictory
and gets flagged by security scanners. Remove X-Frame-Options entirely.
Also replace wildcard *.worldmonitor.app with explicit subdomain list
to limit the framing scope to known variants only.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Vercel project settings already redirect non-www → www (307).
The added www→non-www redirect in vercel.json conflicts, creating
an infinite redirect loop. Removing the vercel.json redirect.
The Turnstile 403 fix is to add www.worldmonitor.app to the widget's
allowed hostnames in the Cloudflare Dashboard instead.
- geolocation=() → geolocation=(self): the app uses navigator.geolocation
for user location features, was blocked by the overly restrictive policy
- Add challenges.cloudflare.com to picture-in-picture allow list: Turnstile
widget iframe needs PiP permission
- Add 301 redirect from www.worldmonitor.app to worldmonitor.app in
vercel.json to fix Turnstile domain mismatch (site key only allows
non-www) causing 403 on form submit.
- Remove stale data-sitekey/theme/size attributes from footer
.cf-turnstile div (explicit rendering sets these via render()).
- Add challenges.cloudflare.com to script-src and frame-src in CSP
- Use same-origin /register-interest instead of cross-origin api.worldmonitor.app
(avoids CORS preflight failures when served from www.worldmonitor.app)
- Rebuild pro page bundle with the fix
* perf: reduce Vercel data transfer costs with CDN optimization
- Increase polling intervals (markets 8→12min, feeds 15→20min, crypto 8→12min)
- Increase background tab hiddenMultiplier from 10→30 (polls 3x less when hidden)
- Double CDN s-maxage TTLs across all cache tiers in gateway
- Add CDN-Cache-Control header for Cloudflare-specific longer edge caching
- Add ETag generation + 304 Not Modified support in gateway (zero-byte revalidation)
- Add CDN-Cache-Control to bootstrap endpoint
- Add explicit SPA rewrite rule in vercel.json for CF proxy compatibility
- Add Cache-Control headers for /map-styles/, /data/, /textures/ static paths
* fix: prevent CF from caching SPA HTML + reduce Polymarket bandwidth 95%
- vercel.json: apply no-cache headers to ALL SPA routes (same regex as
rewrite rule), not just / and /index.html — prevents CF proxy from
caching stale HTML that references old content-hashed bundle filenames
- Polymarket: add server-side aggregation via Railway seed script that
fetches all tags once and writes to Redis, eliminating 11-request
fan-out per user per poll cycle
- Bootstrap: add predictions to hydration keys for zero-cost page load
- RPC handler: read Railway-seeded bootstrap key before falling back to
live Gamma API fetch
- Client: 3-strategy waterfall (bootstrap → RPC → fan-out fallback)
AviationStack API calls cost ~$100/day because each cache miss triggered
114 individual API calls from Vercel Edge (where isolates don't share
in-flight dedup). This moves all AviationStack fetching to the Railway
relay (like market data, OREF, UCDP) and reduces to 40 top international
hubs (down from 114).
- Add AVIATIONSTACK_AIRPORTS constant (40 curated IATA codes)
- Add startAviationSeedLoop() to ais-relay.cjs (2h interval, 4h TTL)
- Make Vercel handler cache-read-only (getCachedJson + simulation fallback)
- Delete Vercel cron (warm-aviation-cache.ts) and remove from vercel.json
- Add wildcard CORS headers in vercel.json for /api/* routes so Vercel
infra 500s (which bypass edge function code) still include CORS headers
- Bump rate limit from 300 to 600 req/60s in both rate-limit files to
accommodate dashboard init burst (~30-40 parallel requests)
- Add smartraveller.gov.au (bare + www) to Railway relay RSS allowlist
- Add redirect hostname validation in fetchWithRedirects to prevent SSRF
via open redirects on allowed domains
Vercel Firewall injects an obfuscated bot-protection challenge script
into served pages. Our hash-based script-src in vercel.json blocked
this injected script, causing CSP violations and SyntaxError.
The fix uses a dual-policy approach:
- vercel.json header: 'unsafe-inline' (allows Vercel challenge scripts)
- index.html <meta> tag: strict hash-based (enforced on our pages)
When both CSP policies exist, browsers enforce both — our <meta> tag's
strict hashes still protect our actual pages.
Vercel routes dynamic [domain] segments BEFORE specific directory names,
so api/[domain]/v1/[rpc].ts was catching every request meant for
api/intelligence/v1/[rpc].ts, api/economic/v1/[rpc].ts, etc. and
returning 404. Delete the catch-all so requests reach the real handlers.
Also: add missing CSP script hash, add list-iran-events cache tier,
and update route-cache-tier test to read from server/gateway.ts.
Remove 'unsafe-inline' from script-src in both index.html and vercel.json,
replacing it with SHA-256 hashes of the two static inline theme-detection
scripts. Add security.txt, sitemap.xml, and Sitemap directive in robots.txt
to improve scanner reputation. Fix stale variant-metadata test that was
reading vite.config.ts instead of the extracted variant-meta.ts module.
Expand Permissions-Policy from 3 to 19 directives (16 fully disabled,
3 delegated to YouTube origins for embed compatibility). Remove
unencrypted ws: and dev-only http://localhost:5173 from production CSP
connect-src. Add 5 guardrail tests to prevent regressions.
- Remove PostHog analytics runtime and configuration
- Add API rate limiting (api/_rate-limit.js)
- Harden traffic controls across edge functions
- Add runtime fallback controls and data-loader improvements
- Add military base data scripts (fetch-mirta-bases, fetch-osm-bases)
- Gitignore large raw data files
- Settings playground prototypes
Vercel's :path* wildcard doesn't match trailing slashes that
PostHog SDK appends (e.g. /ingest/s/?compression=...), causing 404s.
Switch to :path(.*) which matches all path segments including
trailing slashes. Ref: PostHog/posthog#17596
Add X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security,
Content-Security-Policy, Referrer-Policy, and Permissions-Policy headers
to all routes via vercel.json catch-all pattern.
Invert the path logic from an allowlist of watched directories to an
exclusion list (*.md, .planning, docs, e2e, scripts, .github). This
brings the command from 318 to 242 characters while keeping the same
build-trigger behavior for all source code changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add /ingest/* rewrites in vercel.json → us.i.posthog.com
- Web uses /ingest proxy, desktop uses direct PostHog host
- Enable capture_pageview so every visitor registers
Guard ignoreCommand against unset VERCEL_GIT_PREVIOUS_SHA (first deploy,
force deploy) which caused git diff to fail → build canceled. Also add
data/ to watched paths.
- fred-data: batch mode (comma-separated series_id) reduces 7 edge
function invocations to 1; cap at 15 series; propagate upstream
502s instead of masking as empty 200; add X-Data-Status header
- ucdp-events: parallelize page fetches; track failed pages and use
short cache TTL for partial results instead of caching at full 6h
- ucdp: add OPTIONS/method guard matching ucdp-events pattern
- middleware: exact-match social bot paths instead of startsWith
- vercel.json: use VERCEL_GIT_PREVIOUS_SHA for multi-commit diffs;
add middleware.ts, settings.html, vercel.json to watch list
- Panel.ts: use safeHtml() allowlist sanitizer for tooltip content
- dom-utils: add safeHtml() with tag/attribute allowlist and
javascript: URI blocking
- vercel.json: add no-cache headers for / and /index.html so CDN never serves stale HTML
- main.ts: add vite:preloadError listener to auto-reload once on chunk 404s
- vite.config.ts: remove HTML from SW precache, add NetworkFirst for navigation requests