mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
e9146516a56fd52bd771fe573c3a9a153ce7fc39
67 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d75bde4e03 |
fix(agent-readiness): host-aware oauth-protected-resource endpoint (#3351)
* fix(agent-readiness): host-aware oauth-protected-resource endpoint
isitagentready.com enforces that `authorization_servers[*]` share
origin with `resource` (same-origin rule, matches Cloudflare's
mcp.cloudflare.com reference — RFC 9728 §3 permits split origins
but the scanner is stricter).
A single static file served from 3 hosts (apex/www/api) can only
satisfy one origin at a time. Replacing with an edge function that
derives both `resource` and `authorization_servers` from the
request `Host` header gives each origin self-consistent metadata.
No server-side behavior changes: api/oauth/*.js token issuer
doesn't bind tokens to a specific resource value (verified in
the previous PR's review).
* fix(agent-readiness): host-derive resource_metadata + runtime guardrails
Addresses P1/P2 review on this PR:
- api/mcp.ts (P1): WWW-Authenticate resource_metadata was still
hardcoded to apex even when the client hit api.worldmonitor.app.
Derive from request.headers.get('host') so each client gets a
pointer matching their own origin — consistent with the host-
aware edge function this PR introduces.
- api/oauth-protected-resource.ts (P2): add Vary: Host so any
intermediate cache keys by hostname (belt + suspenders on top of
Vercel's routing).
- tests/deploy-config.test.mjs (P2): replace regex-on-source with
a runtime handler invocation asserting origin-matching metadata
for apex/www/api hosts, and tighten the api/mcp.ts assertion to
require host-derived resource_metadata construction.
---------
Co-authored-by: Elie Habib <elie@worldmonitor.app>
|
||
|
|
dff14ed344 |
feat(agent-readiness): RFC 9727 API catalog + native openapi.yaml serve (#3343)
* feat(agent-readiness): RFC 9727 API catalog + native openapi.yaml serve Closes #3309, part of epic #3306. - Serves the sebuf-bundled OpenAPI spec natively at https://www.worldmonitor.app/openapi.yaml with correct application/yaml content-type (no Mintlify proxy hop). Build-time copy from docs/api/worldmonitor.openapi.yaml. - Publishes RFC 9727 API catalog at /.well-known/api-catalog with service-desc pointing at the native URL, status rel pointing at /api/health, and a separate anchor for the MCP endpoint referencing its SEP-1649 card (#3311). Refs PR #3341 (sebuf v0.11.1 bundle landed). * test(deploy-config): update SPA catch-all regex assertion The deploy-config guardrail hard-codes the SPA catch-all regex string and asserts its Cache-Control is no-cache. The prior commit added openapi.yaml to the exclusion list; this updates the test to match so the guardrail continues to protect HTML entry caching. * fix(agent-readiness): address Greptile review on PR #3343 - Extract openapi.yaml copy into named script `build:openapi` and prefix every web-variant build (build:full/tech/finance/happy/ commodity). prebuild delegates to the same script so the default `npm run build` path is unchanged. Swap shell `cp` for Node's cpSync for cross-platform safety. - Bump service-desc MIME type in /.well-known/api-catalog from application/yaml to application/vnd.oai.openapi (IANA-registered OpenAPI media type). Endpoint Content-Type stays application/yaml for browser/tool compatibility. * fix(agent-readiness): P1 health href + guardrail tests on PR #3343 - status.href in /.well-known/api-catalog was pointing at https://api.worldmonitor.app/health (which serves the SPA HTML, not a health response). Corrected to /api/health, which returns the real {"status":"HEALTHY",...} JSON from api/health.js. - Extend tests/deploy-config.test.mjs with assertions that would have caught this regression: linkset structure, status/service- desc href shapes, and presence of build:openapi across every web-variant build script. |
||
|
|
2789b07095 |
chore(vercel): RFC 9116 canonical /security.txt → /.well-known/security.txt 301 (#3326)
Per agent-readiness epic #3306 follow-up: legacy /security.txt path was returning the SPA shell (text/html) because of the catch-all rewrite. Add a permanent redirect so both apex and www emit 301 → /.well-known/security.txt, matching RFC 9116 §3. No-op for the canonical /.well-known/security.txt path, which already serves correctly. |
||
|
|
4853645d53 |
fix(brief): switch carousel to @vercel/og on edge runtime (#3210)
* fix(brief): switch carousel to @vercel/og on edge runtime Every attempt to ship the Phase 8 Telegram carousel on Vercel's Node serverless runtime has failed at cold start: - PR #3174 direct satori + @resvg/resvg-wasm: Vercel edge bundler refused the `?url` asset import required by resvg-wasm. - PR #3174 (fix) direct satori + @resvg/resvg-js native binding: Node runtime accepted it, but Vercel's nft tracer does not follow @resvg/resvg-js/js-binding.js's conditional `require('@resvg/resvg-js-<platform>-<arch>-<libc>')` pattern, so the linux-x64-gnu peer package was never bundled. Cold start threw MODULE_NOT_FOUND, isolate crashed, FUNCTION_INVOCATION_FAILED on every request including OPTIONS, and Telegram reported WEBPAGE_CURL_FAILED with no other signal. - PR #3204 added `vercel.json` `functions.includeFiles` to force the binding in, but (a) the initial key was a literal path that Vercel micromatch read as a character class (PR #3206 fixed), (b) even with the corrected `api/brief/carousel/**` wildcard, the function still 500'd across the board. The `functions.includeFiles` path appears honored in the deployment manifest but not at runtime for this particular native-binding pattern. Fix: swap the renderer to @vercel/og's ImageResponse, which is Vercel's first-party wrapper around satori + resvg-wasm with Vercel-native bundling. Runs on Edge runtime — matches every other API route in the project. No native binding, no includeFiles, no nft tracing surprises. Cold start ~300ms, warm ~30ms. Changes: - server/_shared/brief-carousel-render.ts: replace renderCarouselPng (Uint8Array) with renderCarouselImageResponse (ImageResponse). Drop ensureLibs + satori + @resvg/resvg-js dynamic-import dance. Keep layout builders (buildCover/buildThreads/buildStory) and font loading unchanged — the Satori object trees are wire-compatible with ImageResponse. - api/brief/carousel/[userId]/[issueDate]/[page].ts: flip `runtime: 'nodejs'` -> `runtime: 'edge'`. Delegate rendering to the renderer's ImageResponse and return it directly; error path still 503 no-store so CDN + Telegram don't pin a bad render. - vercel.json: drop the now-useless `functions.includeFiles` block. - package.json: drop direct `@resvg/resvg-js` and `satori` deps (both now bundled inside @vercel/og). - tests/deploy-config.test.mjs: replace the native-binding regression guards with an assertion that no `functions` block exists (with a comment pointing at the skill documenting the micromatch gotcha for future routes). - tests/brief-carousel.test.mjs: updated comment references. Verified: - typecheck + typecheck:api clean - test:data 5814/5814 pass - node -e test: @vercel/og imports cleanly in Node (tests that reach through the renderer file no longer depend on native bindings) Post-deploy validation: curl -I -H "User-Agent: TelegramBot (like TwitterBot)" \ "https://www.worldmonitor.app/api/brief/carousel/<uid>/<slot>/0" # Expect: HTTP/2 403 (no token) or 200 (valid token) # NOT: HTTP/2 500 FUNCTION_INVOCATION_FAILED Then tail Railway digest logs on the next tick; the `[digest] Telegram carousel 400 ... WEBPAGE_CURL_FAILED` line should stop appearing, and the 3-image preview should actually land on Telegram. * Add renderer smoke test + fix Cache-Control duplication Reviewer flagged residual risk: no dedicated carousel-route smoke test for the @vercel/og path. Adds one, and catches a real bug in the process. Findings during test-writing: 1. @vercel/og's ImageResponse runs CLEANLY in Node via tsx — the comment in brief-carousel.test.mjs saying "we can't test the render in Node" was true for direct satori + @resvg/resvg-wasm but no longer holds after PR #3210. Pure Node render works end-to-end: satori tree-parse, jsdelivr font fetch, resvg-wasm init, PNG output. ~850ms first call, ~20ms warm. 2. ImageResponse sets its own default `Cache-Control: public, immutable, no-transform, max-age=31536000`. Passing Cache-Control via the constructor's headers option APPENDS rather than overrides, producing a duplicated comma-joined value like `public, immutable, no-transform, max-age=31536000, public, max-age=60` on the Response. The route handler was doing exactly this via extraHeaders. Fix: drop our Cache-Control override and rely on @vercel/og's 1-year immutable default — envelope is only immutable for its 7d Redis TTL so the effective ceiling is 7d anyway (after that the route 404s before render). Changes: - tests/brief-carousel.test.mjs: 6 new assertions under `renderCarouselImageResponse`: * renders cover / threads / story pages, each returning a valid PNG (magic bytes + size range) * rejects a structurally empty envelope * threads non-cache extraHeaders onto the Response * pins @vercel/og's Cache-Control default so it survives caller-supplied Cache-Control overrides (regression guard for the bug fixed in this commit) - api/brief/carousel/[userId]/[issueDate]/[page].ts: remove the stacked Cache-Control; lean on @vercel/og default. Drop the now- unused `PAGE_CACHE_TTL` constant. Comment explains why. Verified: - test:data 5820/5820 pass (was 5814, +6 smoke) - typecheck + typecheck:api clean - Render smoke: cover 825ms / threads 23ms / story 16ms first run (wasm init dominates first render) |
||
|
|
56054bfbc1 |
fix(brief): use wildcard glob in vercel.json functions key (PR #3204 follow-up) (#3206)
* fix(brief): use wildcard glob in vercel.json functions key PR #3204 shipped the right `includeFiles` value but the WRONG key: "api/brief/carousel/[userId]/[issueDate]/[page].ts" Vercel's `functions` config keys are micromatch globs, not literal paths. Bracketed segments like `[userId]` are parsed as character classes (match any ONE character from {u,s,e,r,I,d}), so my rule matched zero files and `includeFiles` was silently ignored. Post- merge probe still returned HTTP 500 FUNCTION_INVOCATION_FAILED on every request. Build log shows zero mentions of `carousel` or `resvg` — corroborates the key never applied. Fix: wildcard path segments. "api/brief/carousel/**" Matches any file under the carousel route dir. Since the only deployed file there is the dynamic-segment handler, the effective scope is identical to what I originally intended. Added a second regression test that sweeps every functions key and fails loudly if any bracketed segment slips back in. Guards against future reverts AND against anyone copy-pasting the literal route path without realising Vercel reads it as a glob. 23/23 deploy-config tests pass (was 22, +1 new guard). * Address Greptile P2: widen bracket-literal guard regex Greptile spotted that `/\[[A-Za-z]+\]/` only matches purely-alphabetic segment names. Real-world Next.js routes often use `[user_id]`, `[issue_date]`, `[page1]`, `[slug2024]` — none flagged by the old regex, so the guard would silently pass on the exact kind of regression it was written to catch. Widened to `/\[[A-Za-z][A-Za-z0-9_]*\]/`: - requires a leading letter (so legit char classes like `[0-9]` and `[!abc]` don't false-positive) - allows letters, digits, underscores after the first char - covers every Next.js-style dynamic-segment name convention Also added a self-test that pins positive cases (userId, user_id, issue_date, page1, slug2024) and negative cases (the actual `**` glob, `[0-9]`, `[!abc]`) so any future narrowing of the regex breaks CI immediately instead of silently re-opening PR #3206. 24/24 deploy-config tests pass (was 23, +1 new self-test). |
||
|
|
27849fee1e |
fix(brief): bundle resvg linux-x64-gnu native binding with carousel fn (#3204)
* fix(brief): bundle resvg linux-x64-gnu native binding with carousel fn Real root cause of every Telegram carousel WEBPAGE_CURL_FAILED since PR #3174 merged. Not middleware (last PR fixed that theoretical path but not the observed failure). The Vercel function itself crashes HTTP 500 FUNCTION_INVOCATION_FAILED on every request including OPTIONS - the isolate can't initialise. The handler imports brief-carousel-render which lazy-imports @resvg/resvg-js. That package's js-binding.js does runtime require(@resvg/resvg-js-<platform>-<arch>-<libc>). On Vercel Lambda (Amazon Linux 2 glibc) that resolves to @resvg/resvg-js-linux-x64-gnu. Vercel nft tracing does NOT follow this conditional require so the optional peer package isnt bundled. Cold start throws MODULE_NOT_FOUND, isolate crashes, Vercel returns FUNCTION_INVOCATION_FAILED, Telegram reports WEBPAGE_CURL_FAILED. Fix: vercel.json functions.includeFiles forces linux-x64-gnu binding into the carousel functions bundle. Only this route needs it; every other api route is unaffected. Verified: - deploy-config tests 21/21 pass - JSON valid - Reproduced 500 via curl on all methods and UAs - resvg-js/js-binding.js confirms linux-x64-gnu is the runtime binary on Amazon Linux 2 glibc Post-merge: curl with TelegramBot UA should return 200 image/png instead of 500; next cron tick should clear the Railway [digest] Telegram carousel 400 line. * Address Greptile P2s: regression guard + arch-assumption reasoning Two P2 findings on PR #3204: P2 #1 (inline on vercel.json:6): Platform architecture assumption undocumented. If Vercel migrates to Graviton/arm64 Lambda the cold-start crash silently returns. vercel.json is strict JSON so comments aren't possible inline. P2 #2 (tests/deploy-config.test.mjs:17): No regression guard for the carousel includeFiles rule. A future vercel.json tidy-up could silently revert the fix with no CI signal. Fixed both in a single block: - New describe() in deploy-config.test.mjs asserts the carousel route's functions entry exists AND its includeFiles points at @resvg/resvg-js-linux-x64-gnu. Any drift fails the build. - The block comment above it documents the Amazon Linux 2 x86_64 glibc assumption that would have lived next to the includeFiles entry if JSON supported comments. Includes the Graviton/arm64 migration pointer. tests 22/22 pass (was 21, +1 new). |
||
|
|
8684e5a398 |
fix(brief): per-route CSP override so magazine swipe/arrow nav runs (#3165)
* fix(brief): per-route CSP override so magazine swipe/arrow nav runs The global CSP at /((?!docs).*) allow-lists only four SHA-256 hashes for inline scripts (the app's own index.html scripts). brief-render.js emits its swipe/arrow/wheel/touch nav as a deterministic inline IIFE with a different hash, so the browser silently blocked it. The deck rendered, pages were present, dots were drawn — but nothing advanced. Fix mirrors the existing /api/slack/oauth/callback and /api/discord/oauth/callback precedent: a per-route Content-Security- Policy header for /api/brief/(.*) that relaxes script-src to 'unsafe-inline'. Everything else is tight: - default-src 'self' - connect-src 'self' (no outbound network) - object-src 'none', form-action 'none' - frame-ancestors pinned to worldmonitor domains - style-src keeps Google Fonts; font-src keeps gstatic - script-src keeps Cloudflare Insights beacon (auto-injected) 'unsafe-inline' is safe here because server/_shared/brief-render.js HTML-escapes all Redis-sourced content via escapeHtml over [&<>"']. No user-controlled string reaches the DOM unescaped. Verified: all 17 tests/deploy-config.test.mjs security-header assertions still pass (they target the catch-all route, untouched). * fix(brief): un-block Cloudflare Insights beacon + add CSP test coverage Two P2 follow-ups from Greptile review on #3165. 1. connect-src was 'self' only — the Cloudflare Insights beacon script loaded (script-src allowed static.cloudflareinsights.com) but its outbound POST to https://cloudflareinsights.com/cdn-cgi/rum was silently blocked. Analytics for brief-page traffic was dropped with no console error. Added https://cloudflareinsights.com to connect-src so the beacon can ship its payload. 2. tests/deploy-config.test.mjs had 17 assertions for the catch-all CSP but nothing for the new /api/brief/(.*) override. Any future edit — or accidental deletion — of the rule would land without a red test. Added a 4-test suite covering: - rule exists with a CSP header - script-src allows 'unsafe-inline' (the whole point) - connect-src allows cloudflareinsights.com (this fix) - tight non-script defaults still present (default-src 'self', object-src 'none', form-action 'none', base-uri 'self') 21/21 deploy-config assertions pass locally. |
||
|
|
aa01033dbd |
fix(csp): add Stripe to payment Permissions-Policy (#2807)
* fix(csp): add Stripe domains to payment Permissions-Policy Stripe 3D Secure fingerprint iframe needs the payment permission. Without hooks.stripe.com and js.stripe.com in the payment directive, browsers block the Payment Request API inside Stripe's iframe. * test: update deploy-config test for new Stripe payment permission |
||
|
|
53801cae5c |
fix(csp): allow Stripe 3D Secure frames + consolidate Dodo CSP entries (#2806)
Dodo Payments uses Stripe under the hood for card payment 3D Secure verification. hooks.stripe.com and js.stripe.com were not in frame-src or script-src, causing the payment overlay to fail with CSP violations. Consolidated individual Dodo CSP entries (checkout.dodopayments.com, test.checkout.dodopayments.com, *.hs.dodopayments.com, etc.) into a single *.dodopayments.com wildcard per Dodo's recommended allowlist. |
||
|
|
e7e7601c13 |
fix(csp): allow Turnstile picture-in-picture and xr-spatial-tracking (#2792)
Cloudflare Turnstile probes these permissions for bot detection fingerprinting, causing console violations on /pro. Grant them to challenges.cloudflare.com to silence the noise. |
||
|
|
6148d4ca75 |
fix(csp): allow Dodo payment frames + Google Pay permission (#2789)
- frame-src: added *.hs.dodopayments.com, *.custom.hs.dodopayments.com, pay.google.com (Dodo payment iframe and Google Pay) - Permissions-Policy: payment now allowed for checkout.dodopayments.com and pay.google.com (was denied entirely) |
||
|
|
df18d2241e |
fix(csp): allow Dodo SDK from *.hs.dodopayments.com (#2647)
* fix(csp): widen Dodo SDK script-src to https://*.hs.dodopayments.com CSP blocked https://sdk.hs.dodopayments.com (Sentry 7368303076). Only https://sdk.custom.hs.dodopayments.com was allowed. Dodo SDK loads from both subdomains. Wildcarded to cover both. * fix(csp): add *.custom.hs.dodopayments.com for multi-level subdomain CSP wildcard *.hs.dodopayments.com only covers one subdomain level, so sdk.custom.hs.dodopayments.com (two levels) was still blocked. Added explicit *.custom.hs.dodopayments.com to cover both patterns. |
||
|
|
6a7749b126 | fix(csp): allow Dodo Payments SDK script origin in script-src (#2631) | ||
|
|
9893bb1cf2 |
feat: Dodo Payments integration + entitlement engine & webhook pipeline (#2024)
* feat(14-01): install @dodopayments/convex and register component - Install @dodopayments/convex@0.2.8 with peer deps satisfied - Create convex/convex.config.ts with defineApp() and dodopayments component - Add TODO for betterAuth registration when PR #1812 merges Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(14-01): extend schema with 6 payment tables for Dodo integration - Add subscriptions table with status enum, indexes, and raw payload - Add entitlements table (one record per user) with features blob - Add customers table keyed by userId with optional dodoCustomerId - Add webhookEvents table for full audit trail (retained forever) - Add paymentEvents table for billing history (charge/refund) - Add productPlans table for product-to-plan mapping in DB - All existing tables (registrations, contactMessages, counters) unchanged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(14-01): add auth stub, env helper, and Dodo env var docs - Create convex/lib/auth.ts with resolveUserId (returns test-user-001 in dev) and requireUserId (throws on unauthenticated) as sole auth entry points - Create convex/lib/env.ts with requireEnv for runtime env var validation - Append DODO_API_KEY, DODO_WEBHOOK_SECRET, DODO_PAYMENTS_WEBHOOK_SECRET, and DODO_BUSINESS_ID to .env.example with setup instructions - Document dual webhook secret naming (library vs app convention) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(14-02): add seed mutation for product-to-plan mappings - Idempotent upsert mutation for 5 Dodo product-to-plan mappings - Placeholder product IDs to be replaced after Dodo dashboard setup - listProductPlans query for verification and downstream use Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(14-02): populate seed mutation with real Dodo product IDs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(15-02): add plan-to-features entitlements config map - Define PlanFeatures type with 5 feature dimensions - Add PLAN_FEATURES config for 6 tiers (free through enterprise) - Export getFeaturesForPlan helper with free-tier fallback - Export FREE_FEATURES constant for default entitlements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(15-01): add webhook HTTP endpoint with signature verification - Custom httpAction verifying Dodo webhook signatures via @dodopayments/core - Returns 400 for missing headers, 401 for invalid signature, 500 for processing errors - HTTP router at /dodopayments-webhook dispatches POST to webhook handler - Synchronous processing before 200 response (within Dodo 15s timeout) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(15-02): add subscription lifecycle handlers and entitlement upsert - Add upsertEntitlements helper (creates/updates per userId, no duplicates) - Add isNewerEvent guard for out-of-order webhook rejection - Add handleSubscriptionActive (creates subscription + entitlements) - Add handleSubscriptionRenewed (extends period + entitlements) - Add handleSubscriptionOnHold (pauses without revoking entitlements) - Add handleSubscriptionCancelled (preserves entitlements until period end) - Add handleSubscriptionPlanChanged (updates plan + recomputes entitlements) - Add handlePaymentEvent (records charge events for succeeded/failed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(15-01): add idempotent webhook event processor with dispatch skeleton - processWebhookEvent internalMutation with idempotency via by_webhookId index - Switch dispatch for 7 event types: 5 subscription + 2 payment events - Stub handlers log TODO for each event type (to be implemented in Plan 03) - Error handling marks failed events and re-throws for HTTP 500 + Dodo retry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(15-01): complete webhook endpoint plan - Update auto-generated api.d.ts with new payment module types - SUMMARY, STATE, and ROADMAP updated (.planning/ gitignored) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(15-03): wire subscription handlers into webhook dispatch - Replace 6 stub handler functions with imports from subscriptionHelpers - All 7 event types (5 subscription + 2 payment) dispatch to real handlers - Error handling preserves failed event status in webhookEvents table - Complete end-to-end pipeline: HTTP action -> mutation -> handler functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(15-04): install convex-test, vitest, and edge-runtime; configure vitest - Add convex-test, vitest, @edge-runtime/vm as dev dependencies - Create vitest.config.mts scoped to convex/__tests__/ with edge-runtime environment - Add test:convex and test:convex:watch npm scripts * test(15-04): add 10 contract tests for webhook event processing pipeline - Test all 5 subscription lifecycle events (active, renewed, on_hold, cancelled, plan_changed) - Test both payment events (succeeded, failed) - Test deduplication by webhook-id (same id processed only once) - Test out-of-order event rejection (older timestamp skipped) - Test subscription reactivation (cancelled -> active on same subscription_id) - Verify entitlements created/updated with correct plan features * fix(15-04): exclude __tests__ from convex typecheck convex-test uses Vite-specific import.meta.glob and has generic type mismatches with tsc. Tests run correctly via vitest; excluding from convex typecheck avoids false positives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(16-01): add tier levels to PLAN_FEATURES and create entitlement query - Add tier: number to PlanFeatures type (0=free, 1=pro, 2=api, 3=enterprise) - Add tier values to all plan entries in PLAN_FEATURES config - Create convex/entitlements.ts with getEntitlementsForUser public query - Free-tier fallback for missing or expired entitlements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(16-01): create Redis cache sync action and wire upsertEntitlements - Create convex/payments/cacheActions.ts with syncEntitlementCache internal action - Wire upsertEntitlements to schedule cache sync via ctx.scheduler.runAfter(0, ...) - Add deleteRedisKey() to server/_shared/redis.ts for explicit cache invalidation - Redis keys use raw format (entitlements:{userId}) with 1-hour TTL - Cache write failures logged but do not break webhook pipeline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(16-02): add entitlement enforcement to API gateway - Create entitlement-check middleware with Redis cache + Convex fallback - Replace PREMIUM_RPC_PATHS boolean Set with ENDPOINT_ENTITLEMENTS tier map - Wire checkEntitlement into gateway between API key and rate limiting - Add raw parameter to setCachedJson for user-scoped entitlement keys - Fail-open on missing auth/cache failures for graceful degradation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(16-03): create frontend entitlement service with reactive ConvexClient subscription - Add VITE_CONVEX_URL to .env.example for frontend Convex access - Create src/services/entitlements.ts with lazy-loaded ConvexClient - Export initEntitlementSubscription, onEntitlementChange, getEntitlementState, hasFeature, hasTier, isEntitled - ConvexClient only loaded when userId available and VITE_CONVEX_URL configured - Graceful degradation: log warning and skip when Convex unavailable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(16-04): add 6 contract tests for Convex entitlement query - Free-tier defaults for unknown userId - Active entitlements for subscribed user - Free-tier fallback for expired entitlements - Correct tier mapping for api_starter and enterprise plans - getFeaturesForPlan fallback for unknown plan keys Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(16-04): add 6 unit tests for gateway entitlement enforcement - getRequiredTier: gated vs ungated endpoint tier lookup - checkEntitlement: ungated pass-through, missing userId graceful degradation - checkEntitlement: 403 for insufficient tier, null for sufficient tier - Dependency injection pattern (_testCheckEntitlement) for clean testability - vitest.config.mts include expanded to server/__tests__/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(17-01): create Convex checkout session action - DodoPayments component wraps checkout with server-side API key - Accepts productId, returnUrl, discountCode, referralCode args - Always enables discount code input (PROMO-01) - Forwards affiliate referral as checkout metadata (PROMO-02) - Dark theme customization for checkout overlay Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(17-03): create PricingSection component with tier cards and billing toggle - 4 tiers: Free, Pro, API, Enterprise with feature comparison - Monthly/annual toggle with "Save 17%" badge for Pro - Checkout buttons using Dodo static payment links - Pro tier visually highlighted with green border and "Most Popular" badge - Staggered entrance animations via motion - Referral code forwarding via refCode prop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(17-01): extract shared ConvexClient singleton, refactor entitlements - Create src/services/convex-client.ts with getConvexClient() and getConvexApi() - Lazy-load ConvexClient via dynamic import to preserve bundle size - Refactor entitlements.ts to use shared client instead of inline creation - Both checkout and entitlement services will share one WebSocket connection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(17-03): integrate PricingSection into App.tsx with referral code forwarding - Import and render PricingSection between EnterpriseShowcase and PricingTable - Pass refCode from getRefCode() URL param to PricingSection for checkout link forwarding - Update navbar CTA and TwoPathSplit Pro CTA to anchor to #pricing section - Keep existing waitlist form in Footer for users not ready to buy - Build succeeds with no new errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update generated files after main merge and pro-test build Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prefix unused ctx param in auth stub to pass typecheck Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(17-04): add 4 E2E contract tests for checkout-to-entitlement flow - Test product plan seeding and querying (5 plans verified) - Test pro_monthly checkout -> webhook -> entitlements (tier=1, no API) - Test api_starter checkout -> webhook -> entitlements (tier=2, apiAccess) - Test expired entitlements fall back to free tier (tier=0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(17-02): install dodopayments-checkout SDK and create checkout overlay service - Install dodopayments-checkout@1.8.0 overlay SDK - Create src/services/checkout.ts with initCheckoutOverlay, openCheckout, startCheckout, showCheckoutSuccess - Dark theme config matching dashboard aesthetic (green accent, dark bg) - Lazy SDK initialization on first use - Fallback to /pro page when Convex is unavailable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(17-02): wire locked panel CTAs and post-checkout return handling - Create src/services/checkout-return.ts for URL param detection and cleanup - Update Panel.ts showLocked() CTA to trigger Dodo overlay checkout (web path) - Keep Tauri desktop path opening URL externally - Add handleCheckoutReturn() call in PanelLayoutManager constructor - Initialize checkout overlay with success banner callback - Dynamic import of checkout module to avoid loading until user clicks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(17-02): add Upgrade to Pro section in UnifiedSettings modal - Add upgrade section at bottom of settings tab with value proposition - Wire CTA button to open Dodo checkout overlay via dynamic import - Close settings modal before opening checkout overlay - Tauri desktop fallback to external URL - Conditionally show "You're on Pro" when user has active entitlement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove unused imports in entitlement-check test to pass typecheck:api Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(17-01): guard missing DODO_PAYMENTS_API_KEY with warning instead of silent undefined Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(17-01): use DODO_API_KEY env var name matching Convex dashboard config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(17-02): add Dodo checkout domains to CSP frame-src directive Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(17-03): use test checkout domain for test-mode Dodo products Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(18-01): shared DodoPayments config and customer upsert in webhook - Create convex/lib/dodo.ts centralizing DodoPayments instance and API exports - Refactor checkout.ts to import from shared config (remove inline instantiation) - Add customer record upsert in handleSubscriptionActive for portal session support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(18-01): billing queries and actions for subscription management - Add getSubscriptionForUser query (plan status, display name, renewal date) - Add getCustomerByUserId and getActiveSubscription internal queries - Add getCustomerPortalUrl action (creates Dodo portal session via SDK) - Add changePlan action (upgrade/downgrade with proration via Dodo SDK) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(18-02): add frontend billing service with reactive subscription watch - SubscriptionInfo interface for plan status display - initSubscriptionWatch() with ConvexClient onUpdate subscription - onSubscriptionChange() listener pattern with immediate fire for late subscribers - openBillingPortal() with Dodo Customer Portal fallback - changePlan() with prorated_immediately proration mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(18-02): add subscription status display and Manage Billing button to settings - Settings modal shows plan name, status badge, and renewal date for entitled users - Status-aware colors: green (active), yellow (on_hold), red (cancelled/expired) - Manage Billing button opens Dodo Customer Portal via billing service - initSubscriptionWatch called at dashboard boot alongside entitlements - Import initPaymentFailureBanner for Task 3 wiring Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(18-02): add persistent payment failure banner for on_hold subscriptions - Red fixed-position banner at top of dashboard when subscription is on_hold - Update Payment button opens Dodo billing portal - Dismiss button with sessionStorage persistence (avoids nagging in same session) - Auto-removes when subscription returns to active (reactive via Convex) - Event listeners attached directly to DOM (not via debounced setContent) - Wired into panel-layout constructor alongside subscription watch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address code review — identity bridge, entitlement gating, fail-closed, env hygiene P0: Checkout-to-user identity bridge - Pass userId as metadata.wm_user_id in checkout sessions - Webhook resolveUserId: try metadata first, then customer table, then dev-only fallback - Fail closed in production when no user identity can be resolved P0: Unify premium gating to read Dodo entitlements - data-loader.ts: hasPremiumAccess() checks isEntitled() || API key - panels.ts: isPanelEntitled checks isEntitled() before API key fallback - panel-layout.ts: reload on entitlement change to unlock panels P1: Fail closed on unknown product IDs - resolvePlanKey throws on unmapped product (webhook retries) - getFeaturesForPlan throws on unknown planKey P1: Env var hygiene - Canonical DODO_API_KEY (no dual-name fallback in dodo.ts) - console.error on missing key instead of silent empty string P1: Fix test suite scheduled function errors - Guard scheduler.runAfter with UPSTASH_REDIS_REST_URL check - Tests skip Redis cache sync, eliminating convex-test write errors P2: Webhook rollback durability - webhookMutations: return error instead of rethrow (preserves audit row) - webhookHandlers: check mutation return for error indicator P2: Product ID consolidation - New src/config/products.ts as single source of truth - Panel.ts and UnifiedSettings.ts import from shared config P2: ConvexHttpClient singleton in entitlement-check.ts P2: Concrete features validator in schema (replaces v.any()) P2: Tests seed real customer mapping (not fallback user) P3: Narrow eslint-disable to typed interfaces in subscriptionHelpers P3: Real ConvexClient type in convex-client.ts P3: Better dev detection in auth.ts (CONVEX_IS_DEV) P3: Add VITE_DODO_ENVIRONMENT to .env.example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(payments): security audit hardening — auth gates, webhook retry, transaction safety - Gate all public billing/checkout endpoints with resolveUserId(ctx) auth check - Fix webhook retry: record events after processing, not before; delete failed events on retry - Fix transaction atomicity: let errors propagate so Convex rolls back partial writes - Fix isDevDeployment to use CONVEX_IS_DEV (same as lib/auth.ts) - Add missing handlers: subscription.expired, refund.*, dispute.* - Fix toEpochMs silent fallback — now warns on missing billing dates - Use validated payload directly instead of double-parsing webhook body - Fix multi-sub query to prioritize active > on_hold > cancelled > expired - Change .unique() to .first() on customer lookups (defensive against duplicates) - Update handleSubscriptionActive to patch planKey/dodoProductId on existing subs - Frontend: portal URL validation, getProWidgetKey(), subscription cleanup on destroy - Make seedProductPlans internalMutation, console.log → console.warn for ops signals Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address code review — identity bridge, entitlement gating, fail-safe dev detection P0: convex/lib/auth.ts + subscriptionHelpers.ts — remove CONVEX_CLOUD_URL heuristic that could treat production as dev. Now uses ctx.auth.getUserIdentity() as primary auth with CONVEX_IS_DEV-only dev fallback. P0: server/gateway.ts + auth-session.ts — add bearer token (Clerk JWT) support for tier-gated endpoints. Authenticated users bypass API key requirement; userId flows into x-user-id header for entitlement check. Activated by setting CLERK_JWT_ISSUER_DOMAIN env var. P1: src/services/user-identity.ts — centralized getUserId() replacing scattered getProWidgetKey() calls in checkout.ts, billing.ts, panel-layout.ts. P2: src/App.ts — premium panel prime/refresh now checks isEntitled() alongside WORLDMONITOR_API_KEY so Dodo-entitled web users get data loading. P2: convex/lib/dodo.ts + billing.ts — move Dodo SDK config from module scope into lazy/action-scoped init. Missing DODO_API_KEY now throws at action boundary instead of silently capturing empty string. Tests: webhook test payloads now include wm_user_id metadata (production path). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address P0 access control + P1 identity bridge + P1 entitlement reload loop P0: Remove userId from public billing function args (getSubscriptionForUser, getCustomerPortalUrl, changePlan) — use requireUserId(ctx) with no fallback to prevent unauthenticated callers from accessing arbitrary user data. P1: Add stable anonymous ID (wm-anon-id) in user-identity.ts so createCheckout always passes wm_user_id in metadata. Breaks the infinite webhook retry loop for brand-new purchasers with no auth/localStorage identity. P1: Skip initial entitlement snapshot in onEntitlementChange to prevent reload loop for existing premium users whose shouldUnlockPremium() is already true from legacy signals (API key / wm-pro-key). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address P1 billing flow + P1 anon ID claim path + P2 daily-market-brief P1 — Billing functions wired end-to-end for browser sessions: - getSubscriptionForUser, getCustomerPortalUrl, changePlan now accept userId from args (matching entitlements.ts pattern) with auth-first fallback. Once Clerk JWT is wired into ConvexClient.setAuth(), the auth path will take precedence automatically. - Frontend billing.ts passes userId from getUserId() on all calls. - Subscription watch, portal URL, and plan change all work for browser users with anon IDs. P1 — Anonymous ID → account claim path: - Added claimSubscription(anonId) mutation to billing.ts — reassigns subscriptions, entitlements, customers, and payment events from an anonymous browser ID to the authenticated user. - Documented the anon ID limitation in user-identity.ts with the migration plan (call claimSubscription on first Clerk session). - Created follow-up issue #2078 for the full claim/migration flow. P2 — daily-market-brief added to hasPremiumAccess() block in data-loader.ts loadAllData() so it loads on general data refresh paths (was only in primeVisiblePanelData startup path). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: P0 lock down billing write actions + P2 fix claimSubscription logic P0 — Billing access control locked down: - getCustomerPortalUrl and changePlan converted to internalAction — not callable from the browser, closing the IDOR hole on write paths. - getSubscriptionForUser stays as a public query with userId arg (read-only, matching the entitlements.ts pattern — low risk). - Frontend billing.ts: portal opens generic Dodo URL, changePlan returns "not available" stub. Both will be promoted once Clerk auth is wired into ConvexClient.setAuth(). P2 — claimSubscription merge logic fixed: - Entitlement comparison now uses features.tier first, breaks ties with validUntil (was comparing only validUntil which could downgrade tiers). - Added Redis cache invalidation after claim: schedules deleteEntitlementCache for the stale anon ID and syncEntitlementCache for the real user ID. - Added deleteEntitlementCache internal action to cacheActions.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(billing): strip Dodo vendor IDs from public query response Remove dodoSubscriptionId and dodoProductId from getSubscriptionForUser return — these vendor-level identifiers aren't used client-side and shouldn't be exposed over an unauthenticated fallback path. Addresses koala73's Round 5 P1 review on #2024. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(billing): address koala review cleanup items (P2/P3) - Remove stale dodoSubscriptionId/dodoProductId from SubscriptionInfo interface (server no longer returns them) - Remove dead `?? crypto.randomUUID()` fallback in checkout.ts (getUserId() always returns a string via getOrCreateAnonId()) - Remove unused "failed" status variant and errorMessage from webhookEvents schema (no code path writes them) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add missing isProUser import and allow trusted origins for tier-gated endpoints The typecheck failed because isProUser was used in App.ts but never imported. The unit test failed because the gateway forced API key validation for tier-gated endpoints even from trusted browser origins (worldmonitor.app), where the client-side isProUser() gate controls access instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(gateway): require credentials for premium endpoints regardless of origin Origin header is spoofable — it cannot be a security boundary. Premium endpoints now always require either an API key or a valid bearer token (via Clerk session). Authenticated users (sessionUserId present) bypass the API key check; unauthenticated requests to tier-gated endpoints get 401. Updated test to assert browserNoKey → 401 instead of 200. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tests): add catch-all route to legacy endpoint allowlist The [[...path]].js Vercel catch-all route (domain gateway entry point) was missing from ALLOWED_LEGACY_ENDPOINTS in the edge function tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * revert: remove [[...path]].js from legacy endpoint allowlist This file is Vercel-generated and gitignored — it only exists locally, not in the repo. Adding it to the allowlist caused CI to fail with "stale entry" since the file doesn't exist in CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(payment): apply design system to payment UI (light/dark mode) - checkout.ts: add light themeConfig for Dodo overlay + pass theme flag based on current document.dataset.theme; previously only dark was configured so light-mode users got Dodo's default white UI - UnifiedSettings: replace hardcoded dark hex values (#1a1a1a, #323232, #fff, #909090) in upgrade section with CSS var-driven classes so the panel respects both light and dark themes - main.css: add .upgrade-pro-section / .upgrade-pro-cta / .manage-billing-btn classes using var(--green), var(--bg), var(--surface), var(--border), etc. * fix(checkout): remove invalid theme prop from CheckoutOptions * fix: regenerate package-lock.json with npm 10 (matches CI Node 22) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(gateway): enforce pro role check for authenticated free users on premium paths Free bearer token holders with a valid session bypassed PREMIUM_RPC_PATHS because sessionUserId being set caused forceKey=false, skipping the role check entirely. Now explicitly checks bearer role after API key gate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove unused getSecretState import from data-loader Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: gate Dodo Payments init behind isProUser() (same as Clerk) Entitlement subscription, subscription watch, checkout overlay, and payment banners now only initialize for isProUser() — matching the Clerk auth gate so only wm-pro-key / wm-widget-key holders see it. Also consolidates inline isEntitled()||getSecretState()||role checks in App.ts to use the centralized hasPremiumAccess() from panel-gating. Both gates (Clerk + Dodo) to be removed when ready for all users. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: retrigger workflows * chore: retrigger CI * fix(redis): use POST method in deleteRedisKey for consistency All other write helpers use POST; DEL was implicitly using GET via fetch default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(quick-5): resolve all P1 security issues from koala73 review P1-1: Fail-closed entitlement gate (403 when no userId or lookup fails) P1-2: Checkout requires auth (removed client-supplied userId fallback) P1-3: Removed dual auth (PREMIUM_RPC_PATHS) from gateway, single entitlement path P1-4: Typed features validator in cacheActions (v.object instead of v.any) P1-5: Typed ConvexClient API ref (typeof api instead of Record<string,any>) P1-6: Cache stampede mitigation via request coalescing (_inFlight map) Round8-A: getUserId() returns Clerk user.id, hasUserIdentity() checks real identity Round8-B: JWT verification pinned to algorithms: ['RS256'] - Updated entitlement tests for fail-closed behavior (7 tests pass) - Removed userId arg from checkout client call - Added env-aware Redis key prefix (live/test) - Reduced cache TTL from 3600 to 900 seconds - Added 5s timeout to Redis fetch calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(quick-5): resolve all P2 review issues from koala73 review P2-1: dispute.lost now revokes entitlements (downgrades to free tier) P2-2: rawPayload v.any() documented with JSDoc (intentional: external schema) P2-3: Redis keys prefixed with live/test env (already in P1 commit) P2-4: 5s timeout on Redis fetch calls (already in P1 commit) P2-5: Cache TTL reduced from 3600 to 900 seconds (already in P1 commit) P2-6: CONVEX_IS_DEV warning logged at module load time (once, not per-call) P2-7: claimSubscription uses .first() instead of .unique() (race safety) P2-8: toEpochMs fallback accepts eventTimestamp (all callers updated) P2-9: hasUserIdentity() checks real identity (already in P1 commit) P2-10: Duplicate JWT verification removed (already in P1 commit) P2-11: Subscription queries bounded with .take(10) P2-12: Entitlement subscription exposes destroyEntitlementSubscription() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(convex): resolve TS errors in http.ts after merge Non-null assertions for anyApi dynamic module references and safe array element access in timing-safe comparison. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tests): update gateway tests for fail-closed entitlement system Tests now reflect the new behavior where: - API key + no auth session → 403 (entitlement check requires userId) - Valid bearer + no entitlement data → 403 (fail-closed) - Free bearer → 403 (entitlements unavailable) - Invalid bearer → 401 (no session, forceKey kicks in) - Public routes → 200 (unchanged) The old tests asserted PREMIUM_RPC_PATHS + JWT role behavior which was removed per koala73's P1-3 review (dual auth elimination). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(review): resolve 4 P1 + 2 P2 issues from koala73 round-9 review P1 fixes: - API-key holders bypass entitlement check (were getting 403) - Browser checkout passes userId for identity bridge (ConvexClient has no setAuth yet, so createCheckout accepts optional userId arg) - /pro pricing page embeds wm_user_id in Dodo checkout URL metadata so webhook can resolve identity for first-time purchasers - Remove isProUser() gate from entitlement/billing init — all users now subscribe to entitlement changes so upgrades take effect immediately without manual page reload P2 fixes: - destroyEntitlementSubscription() called on teardown to clear stale premium state across SPA sessions (sign-out / identity change) - Convex queries prefer resolveUserId(ctx) over client-supplied userId; documented as temporary until Clerk JWT wired into ConvexClient.setAuth() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(review): resolve 4 P1 + 1 P2 from koala73 round-10 review P1 — Checkout identity no longer client-controlled: - createCheckout HMAC-signs userId with DODO_PAYMENTS_WEBHOOK_SECRET - Webhook resolveUserId only trusts metadata when HMAC signature is valid; unsigned/tampered metadata is rejected - /pro raw URLs no longer embed wm_user_id (eliminated URL tampering) - Purchases without signed metadata get synthetic "dodo:{customerId}" userId, claimable later via claimSubscription() P1 — IDOR on Convex queries addressed: - Both getEntitlementsForUser and getSubscriptionForUser now reject mismatched userId when the caller IS authenticated (authedUserId != args.userId → return defaults/null) - Created internal getEntitlementsByUserId for future gateway use - Pre-auth fallback to args.userId documented with TODO(clerk-auth) P1 — Clerk identity bridge fixed: - user-identity.ts now uses getCurrentClerkUser() from clerk.ts instead of reading window.Clerk?.user (which was never assigned) - Signed-in Clerk users now correctly resolve to their Clerk user ID P1 — Auth modal available for anonymous users: - Removed isProUser() gate from setupAuthWidget() in App.ts - Anonymous users can now click premium CTAs → sign-in modal opens P2 — .take(10) subscription cap: - Bumped to .take(50) in both getSubscriptionForUser and getActiveSubscription to avoid missing active subscriptions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(review): resolve remaining P2/P3 feedback from koala73 + greptile on PR #2024 JWKS: consolidate duplicate singletons — server/_shared/auth-session.ts now imports the shared JWKS from server/auth-session.ts (eliminates redundant cold-start fetch). Webhook: remove unreachable retry branch — webhookEvents.status is always "processed" (inserted only after success, rolled back on throw). Dead else removed. YAGNI: remove changePlan action + frontend stub (no callers — plan changes use Customer Portal). Remove unused by_status index on subscriptions table. DRY: consolidate identical pro_monthly/pro_annual into shared PRO_FEATURES constant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): read CLERK_JWT_ISSUER_DOMAIN lazily in getJWKS() — fixes CI bearer token tests The shared getJWKS() was reading the env var from a module-scope const, which freezes at import time. Tests set the env var in before() hooks after import, so getJWKS() returned null and bearer tokens were never verified — causing 401 instead of 403 on entitlement checks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(payments): resolve all blocking issues from review rounds 1-2 Data integrity: - Use .first() instead of .unique() on entitlements.by_userId to survive concurrent webhook retries without permanently crashing reads - Add typed paymentEventStatus union to schema; replace dynamic dispute status string construction with explicit lookup map - Document accepted 15-min Redis cache sync staleness bound - Document webhook endpoint URL in .env.example Frontend lifecycle: - Add listeners.clear() to destroyEntitlementSubscription (prevents stale closure reload loop on destroy/re-init cycles) - Add destroyCheckoutOverlay() to reset initialized flag so new layouts can register their success callbacks - Complete PanelLayoutManager.destroy() — add teardown for checkout overlay, payment failure banner, and entitlement change listener - Preserve currentState across destroy; add resetEntitlementState() for explicit reset (e.g. logout) without clearing on every cycle Code quality: - Export DEV_USER_ID and isDev from lib/auth.ts; remove duplicates from subscriptionHelpers.ts (single source of truth) - Remove DODO_PAYMENTS_API_KEY fallback from billing.ts - Document why two Dodo SDK packages coexist (component vs REST) * fix(payments): address round-3 review findings — HMAC key separation, auth wiring, shape guards - Introduce DODO_IDENTITY_SIGNING_SECRET separate from webhook secret (todo 087) Rotating the webhook secret no longer silently breaks userId identity signing - Wire claimSubscription to Clerk sign-in in App.ts (todo 088) Paying anonymous users now have their entitlements auto-migrated on first sign-in - Promote getCustomerPortalUrl to public action + wire openBillingPortal (todo 089) Manage Billing button now opens personalized portal instead of generic URL - Add rate limit on claimSubscription (todo 090) - Add webhook rawPayload shape guard before handler dispatch (todo 096) Malformed payloads return 200 with log instead of crashing the handler - Remove dead exports: resetEntitlementState, customerPortal wrapper (todo 091) - Fix let payload; implicit any in webhookHandlers.ts (todo 092) - Fix test: use internal.* for internalMutation seedProductPlans (todo 095) Co-Authored-By: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com> * fix(payments): address round-4 P1 findings — input validation, anon-id cleanup - claimSubscription: replace broken rate-limit (bypassed for new users, false positives on renewals) with UUID v4 format guard + self-claim guard; prevents cross-user subscription theft via localStorage injection - App.ts: always remove wm-anon-id after non-throwing claim completion (not only when subscriptions > 0); adds optional chaining on result.claimed; prevents cold Convex init on every sign-in for non-purchasers Resolves todos 097, 098, 099, 100 * fix(payments): address round-5 review findings — regex hoist, optional chaining - Hoist ANON_ID_REGEX to module scope (was re-allocated on every call) - Remove /i flag — crypto.randomUUID() always produces lowercase - result.claimed accessed directly (non-optional) — mutation return is typed - Revert removeItem from !client || !api branch — preserve anon-id on infrastructure failure; .catch path handles transient errors * fix(billing): wire setAuth, rebind watches, revoke on dispute.lost, defer JWT, bound collect - convex-client.ts: wire client.setAuth(getClerkToken) so claimSubscription and getCustomerPortalUrl no longer throw 'Authentication required' in production - clerk.ts: expose clearClerkTokenCache() for force-refresh handling - App.ts: rebind entitlement + subscription watches to real Clerk userId on every sign-in (destroy + reinit), fixing stale anon-UUID watches post-claim - subscriptionHelpers.ts: revoke entitlement to 'free' on dispute.lost + sync Redis cache; previously only logged a warning leaving pro access intact after chargeback - gateway.ts: compute isTierGated before resolveSessionUserId; defer JWKS+RS256 verification to inside if (isTierGated) — eliminates JWT work on ~136 non-gated endpoints - billing.ts: .take(1000) on paymentEvents collect — safety bound preventing runaway memory on pathological anonymous sessions before sign-in Closes P1: setAuth never wired (claimSubscription always throws in prod) Closes P2: watch rebind, dispute.lost revocation, gateway perf, unbounded collect * fix(security): address P1 review findings from round-6 audit - Remove _debug block from validateApiKey that contained all valid API keys in envVarRaw/parsedKeys fields (latent full-key disclosure risk) - Replace {db: any} with QueryCtx in getEntitlementsHandler (Convex type safety) - Add pre-insert re-check in upsertEntitlements with OCC race documentation - Fix dispute.lost handler to use eventTimestamp instead of Date.now() for validUntil/updatedAt (preserves isNewerEvent out-of-order replay protection) - Extract getFeaturesForPlan("free") to const in dispute.lost (3x → 1x call) Closes todos #103, #106, #107, #108 * fix(payments): address round-6 open items — throw on shape guard, sign-out cleanup, stale TODOs P2-4: Webhook shape guards now throw instead of returning silently, so Dodo retries on malformed payloads instead of losing events. P3-2: Sign-out branch now destroys entitlement and subscription watches for the previous userId. P3: Removed stale TODO(clerk-auth) comments — setAuth() is wired. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(payments): address codex review — preserve listeners, honest TODOs, retry comment - destroyEntitlementSubscription/destroySubscriptionWatch no longer clear listeners — PanelLayout registers them once and they must survive auth transitions (sign-out → sign-in would lose the premium-unlock reload) - Restore TODO(auth) on entitlements/billing public queries — the userId fallback is a real trust gap, not just a cold-start race - Add comment on webhook shape guards acknowledging Dodo retry budget tradeoff vs silent event loss Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(types): restore non-null assertions in convex/http.ts after merge Main removed ! and as-any casts, but our branch's generated types make anyApi properties possibly undefined. Re-added to fix CI typecheck. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(types): resolve TS errors from rebase — cast internal refs, remove unused imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: regenerate package-lock.json — add missing uqr@0.1.2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(payments): resolve P2/P3 review findings for PR #2024 - Bound and parallelize claimSubscription reads with Promise.all (4x queries -> single round trip; .collect() -> .take() to cap memory) - Add returnUrl allowlist validation in createCheckout to prevent open redirect - Make openBillingPortal return Promise<string | null> for agent-native callers - Extend isCallerPremium with Dodo entitlement tier check (tier >= 1 is premium, unifying Clerk role:pro and Dodo subscriber as two signals for the same gate) - Call resetEntitlementState() on sign-out to prevent entitlement state leakage across sessions (destroyEntitlementSubscription preserves state for reconnects; resetEntitlementState is the explicit sign-out nullifier) - Merge handlePaymentEvent + handleRefundEvent -> handlePaymentOrRefundEvent (type inferred from event prefix; eliminates duplicate resolveUserId call) - Remove _testCheckEntitlement DI export from entitlement-check.ts; inline _checkEntitlementCore into checkEntitlement; tests now mock getCachedJson - Collapse 4 duplicate dispute status tests into test.each - Fix stale entitlement variable name in claimSubscription return value * fix(payments): harden auth and checkout ownership * fix(gateway): tighten auth env handling * fix(gateway): use convex site url fallback * fix(app): avoid redundant checkout resume * fix(convex): cast alertRules internal refs for PR-branch generated types --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Elie Habib <elie.habib@gmail.com> |
||
|
|
ea4fd99ccb |
fix(csp): add www.google.com to frame-src for YouTube embed sub-frames (#2612)
YouTube's iframe player loads google.com in a sub-frame (consent, login, tracking). CSP frame-src was blocking it with disposition:enforce (WORLDMONITOR-HT, 969 events, 513 users). Added to both header and meta tag CSPs. |
||
|
|
9772372a6d |
fix(csp): break Sentry CSP feedback loop causing 446K daily errors (#2600)
* fix(csp): break Sentry CSP feedback loop causing 446K daily errors Root cause: the securitypolicyviolation listener reports violations to Sentry, but the Sentry ingest endpoint itself was blocked by CSP. This triggered a new violation, which tried to report, which got blocked, creating an infinite cascade. The existing sentry.io filter regex didn't match because browsers append :443 to the blocked URI. Three fixes: 1. Fix Sentry filter regex to handle optional :443 port in blocked URI 2. Add Sentry ingest domains to connect-src in both CSPs 3. Sync script-src hashes between vercel.json header and index.html meta tag (removed 4 stale hashes, added 1 missing OAuth hash) When both HTTP header and meta tag CSP exist, the browser enforces BOTH independently. Mismatched script-src hashes between them caused legitimate scripts to be blocked by whichever policy lacked their hash. * fix(csp): suppress non-actionable CSP violations flooding Sentry (446K/day) Root cause: the securitypolicyviolation listener (PR #2365) reports ALL violations to Sentry. Dual CSP (header + meta tag) fires violations for first-party API calls (api.worldmonitor.app) that actually succeed with HTTP 200. These are not real blocks. Fixes: 1. Skip report-only disposition (e.disposition !== 'enforce') 2. Skip first-party origins (*.worldmonitor.app) — dual-CSP quirk fires violations for requests the other policy allows 3. Host-based Sentry filter (sentry.io anywhere, not just /api/ path) to handle origin-only blocked URIs (e.g., sentry.io:443) 4. Sync script-src hashes between vercel.json and index.html meta tag (removed 4 stale hashes, added 1 missing OAuth hash) 5. Add Sentry ingest to connect-src (defense-in-depth, not primary fix) 6. Add test: CSP script-src hash parity between header and meta tag The explicit Sentry ingest additions to connect-src are redundant with the existing https: scheme-source but serve as documentation and defense-in-depth. |
||
|
|
29bf4aa3c8 |
feat(discord): Discord notification channel via OAuth2 webhook.incoming (#2596)
* feat(discord): add discord to channelTypeValidator and notificationChannels schema * feat(discord): add setDiscordOAuthChannelForUser mutation and set-discord-oauth HTTP handler * feat(discord): add OAuth2 webhook.incoming start and callback edge functions * feat(discord): add sendDiscord delivery function with SSRF guard, 429 retry, and deactivation * feat(discord): add frontend Connect Discord UI and vercel.json CSP header * fix(discord): reject discord in generic setChannel/setChannelForUser — must use set-discord-oauth * fix(discord): fix reflected XSS in OAuth callbacks, cap Discord content at 2000 chars, add retry depth guard |
||
|
|
0c738d7299 |
fix(slack-oauth): CSP blocked inline script in callback popup (#2576)
* 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. |
||
|
|
be679e8663 |
fix(oauth): bypass form-action CSP blocking Connectors UI form submission (#2438)
* 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. |
||
|
|
51f7b7cf6d |
feat(mcp): full OAuth 2.1 compliance — Authorization Code + PKCE + DCR (#2432)
* 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. |
||
|
|
14a31c4283 |
feat(mcp): OAuth 2.0 Authorization Server for claude.ai connector (#2418)
* feat(mcp): add OAuth 2.0 Authorization Server for claude.ai connector Implements spec-compliant MCP authentication so claude.ai's remote connector (which requires OAuth Client ID + Secret, no custom headers) can authenticate. - public/.well-known/oauth-authorization-server: RFC 8414 discovery document - api/oauth/token.js: client_credentials grant, issues UUID Bearer token in Redis TTL 3600s - api/_oauth-token.js: resolveApiKeyFromBearer() looks up token in Redis - api/mcp.ts: 3-tier auth (Bearer OAuth first, then ?key=, then X-WorldMonitor-Key); switch to getPublicCorsHeaders; surface error messages in catch - vercel.json: rewrite /oauth/token, exclude oauth from SPA, CORS headers - tests: update SPA no-cache pattern Supersedes PR #2417. Usage: URL=worldmonitor.app/mcp, Client ID=worldmonitor, Client Secret=<API key> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: fix markdown lint in OAuth plan (blank lines around lists) * fix(oauth): address all P1+P2 code review findings for MCP OAuth endpoint - Add per-IP rate limiting (10 req/min) to /oauth/token via Upstash slidingWindow - Return HTTP 401 + WWW-Authenticate header when Bearer token is invalid/expired - Add Cache-Control: no-store + Pragma: no-cache to token response (RFC 6749 §5.1) - Simplify _oauth-token.js to delegate to readJsonFromUpstash (removes duplicated Redis boilerplate) - Remove dead code from token.js: parseBasicAuth, JSON body path, clientId/issuedAt fields - Add Content-Type: application/json header for /.well-known/oauth-authorization-server - Remove response_types_supported (only applies to authorization endpoint, not client_credentials) Closes: todos 075, 076, 077, 078, 079 🤖 Generated with claude-sonnet-4-6 via Claude Code (https://claude.ai/claude-code) + Compound Engineering v2.40.0 Co-Authored-By: claude-sonnet-4-6 (200K context) <noreply@anthropic.com> * chore(review): fresh review findings — todos 081-086, mark 075/077/078/079 complete * fix(mcp): remove ?key= URL param auth + mask internal errors - Remove ?key= query param auth path — API keys in URLs appear in Vercel/CF access logs, browser history, Referer headers. OAuth client_credentials (same PR) already covers clients that cannot set custom headers. Only two auth paths remain: Bearer OAuth and X-WorldMonitor-Key header. - Revert err.message disclosure: catch block was accidentally exposing internal service URLs/IPs via err.message. Restore original hardcoded string, add console.error for server-side visibility. Resolves: todos 081, 082 * fix(oauth): resolve all P2/P3 review findings (todos 076, 080, 083-086) - 076: no-credentials path in mcp.ts now returns HTTP 401 + WWW-Authenticate instead of rpcError (200) - 080: store key fingerprint (sha256 first 16 hex chars) in Redis, not plaintext key - 083: replace Array.includes() with timingSafeIncludes() (constant-time HMAC comparison) in token.js and mcp.ts - 084: resolveApiKeyFromBearer uses direct fetch that throws on Redis errors (500 not 401 on infra failure) - 085: token.js imports getClientIp, getPublicCorsHeaders, jsonResponse from shared helpers; removes local duplicates - 086: mcp.ts auth chain restructured to check Bearer header first, passes token string to resolveApiKeyFromBearer (eliminates double header read + unconditional await) * test(mcp): update auth test to expect HTTP 401 for missing credentials Align with todo 076 fix: no-credentials path now returns 401 + WWW-Authenticate instead of JSON-RPC 200 response. Also asserts WWW-Authenticate header presence. * chore: mark todos 076, 080, 083-086 complete * fix(mcp): harden OAuth error paths and fix rate limit cross-user collision - Wrap resolveApiKeyFromBearer() in try/catch in mcp.ts; Redis/network errors now return 503 + Retry-After: 5 instead of crashing the handler - Wrap storeToken() fetch in try/catch in oauth/token.js; network errors return false so the existing if (!stored) path returns 500 cleanly - Re-key token endpoint rate limit by sha256(clientSecret).slice(0,8) instead of IP; prevents cross-user 429s when callers share Anthropic's shared outbound IPs (Claude remote MCP connector) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
1635ad3898 |
feat(mcp): expose /mcp as clean public endpoint (#2411)
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. |
||
|
|
47f0dd133d |
fix(widgets): restore iframe content after drag, remove color-cycle button (#2368)
* 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
|
||
|
|
f9e127471f |
fix(widget): sandbox connect-src cdn.jsdelivr.net + Sentry CSP/5xx tracking (#2365)
* 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 |
||
|
|
93c28cf4e6 |
fix(widgets): fix CSP violations in pro widget iframe (#2362)
* 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 |
||
|
|
990d5a71f5 |
fix(cors): add X-Widget-Key to vercel.json /api/* Allow-Headers (#2316)
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. |
||
|
|
a969a9e3a3 |
feat(auth): integrate clerk.dev (#1812)
* 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> |
||
|
|
32ca22d69f |
feat(analytics): add Umami analytics via self-hosted instance (#1914)
* 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 |
||
|
|
c4a76b2c4a |
fix(security): allow vercel.live and *.vercel.app in CSP for preview deployments (#1887)
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 |
||
|
|
82790e34f6 |
fix(routing): redirect /docs to /docs/documentation (#1804)
/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. |
||
|
|
1f2dafefc0 |
fix(csp): add missing Vercel inline script hash (#1756)
Add sha256-903UI9my1I7mqHoiVeZSc56yd50YoRJTB2269QqL76w= to script-src to allow Vercel-injected inline script that was being blocked by CSP. |
||
|
|
a4e9e5e607 |
fix(docs): exclude /docs from CSP that blocks Mintlify (#1750)
* 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. |
||
|
|
d101c03009 |
fix: unblock geolocation and fix stale CSP hash (#1709)
* 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. |
||
|
|
bcccb3fb9c |
test: cover runtime env guardrails (#1650)
* 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. |
||
|
|
39cf56dd4d |
perf: reduce ~14M uncached API calls/day via client caches + workbox fix + USNI Railway migration (#1605)
* 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. |
||
|
|
987ed03f5d |
feat(webcams): add webcam map layer with Windy API integration (#1540) (#1540)
- Webcam markers on flat, globe, and DeckGL maps with category-based icons - Server-side spatial queries via Redis GEOSEARCH with quantized bbox caching - Pinned webcams panel with localStorage persistence - Seed script for Windy API with regional bounding boxes and adaptive splitting - Input validation (webcamId regex + encodeURIComponent) and NaN projection guards - Bandwidth optimizations: zoom threshold, bbox overlap check, 1s cooldown - Client-side image cache with 200-entry FIFO eviction - Globe altitude-based viewport estimation for webcam loading - CSP updates for webcam iframe sources - Seed-meta key for health.js freshness tracking |
||
|
|
59cd313e16 |
fix(csp): add commodity variant to CSP and fix iframe variant navigation (#1506)
* 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>
|
||
|
|
01a7a791ab |
feat(docs): add Mintlify documentation site at /docs (#1444)
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 |
||
|
|
b65464c0b2 |
feat(blog): add Astro blog at /blog with 16 SEO posts (#1401)
* 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. |
||
|
|
6b2550ff49 |
fix(csp): allow cross-subdomain framing for Pro page variant switcher (#1332)
* 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> |
||
|
|
650c5fba5b |
hotfix: remove www→non-www redirect causing infinite redirect loop (#1201)
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. |
||
|
|
1cfff0f1d2 |
fix: allow geolocation and Turnstile picture-in-picture in Permissions-Policy (#1199)
- 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 |
||
|
|
6f1210156b |
fix(pro): add www→non-www redirect and clean stale Turnstile attributes (#1198)
- 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()). |
||
|
|
f8f1490fcf |
fix(cache): add no-cache header for /pro (exact path) (#1179)
/pro/(.*) only matches subpaths, not /pro itself. CF was caching the /pro HTML page, serving stale bundles after deploys. |
||
|
|
d82c29472a |
fix(pro): CSP allow Turnstile + use same-origin API calls (#1155)
- 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 |
||
|
|
dfc175023a |
feat(pro): Pro waitlist landing page with referral system (#1140)
* fix(desktop): settings UI redesign, IPC security hardening, release profile Settings window: - Add titlebar drag region (macOS traffic light clearance) - Move Export/Import from Overview to Debug & Logs section - Category cards grid changed to 3-column layout Security (IPC trust boundary): - Add require_trusted_window() to get_desktop_runtime_info, open_url, open_live_channels_window_command, open_youtube_login - Validate base_url in open_live_channels_window_command (localhost-only http) Performance: - Add [profile.release] with fat LTO, codegen-units=1, strip, panic=abort - Reuse reqwest::Client via app state with connection pooling - Debounce window resize handler (150ms) in EventHandlerManager * feat(pro): add Pro waitlist landing page with referral system - React 19 + Vite 6 + Tailwind v4 landing page at /pro - Cloudflare Turnstile + honeypot bot protection - Resend transactional confirmation emails with branded template - Viral referral system: unique codes, position tracking, social share - Convex schema: referralCode, referredBy, referralCount fields + counters table - O(1) position counter pattern instead of O(n) collection scan - SEO: structured data, sitemap, scrolling source marquee - Vercel routing: /pro rewrite + cache headers + SPA exclusion - XSS-safe DOM rendering (no innerHTML with user data) |
||
|
|
320786f82a |
fix: prevent CF caching SPA HTML + Polymarket bandwidth optimization (#1058)
* 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) |
||
|
|
278c69b5a3 |
perf: reduce Vercel data transfer costs with CDN optimization (#1057)
- 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 |
||
|
|
6c4901f5da |
fix(aviation): move AviationStack fetching to Railway relay, reduce to 40 airports (#858)
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 |
||
|
|
37f07a6af2 |
fix(prod): CORS fallback, rate-limit bump, RSS proxy allowlist (#814)
- 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 |